Skip to content

Commit

Permalink
Add support for ESM modules and async plugin initializers. (#2915)
Browse files Browse the repository at this point in the history
* Add support for ESM modules and async plugin initialiers.

* Workaround for TypeScript bug.

* Properly await for plugin initialization.
  • Loading branch information
ShogunPanda committed Oct 25, 2021
1 parent 80c2027 commit f26307a
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface PluginInitializer {
*
* @param {API} api
*/
(api: API): void;
(api: API): void | Promise<void>;

}

Expand Down
8 changes: 4 additions & 4 deletions src/childBridgeFork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class ChildBridgeFork {
}
}

loadPlugin(data: ChildProcessLoadEventData): void {
async loadPlugin(data: ChildProcessLoadEventData): Promise<void> {
// set data
this.type = data.type;
this.identifier = data.identifier;
Expand Down Expand Up @@ -100,9 +100,9 @@ export class ChildBridgeFork {
this.externalPortService = new ChildBridgeExternalPortService(this);

// load plugin
this.plugin = this.pluginManager.loadPlugin(data.pluginPath);
this.plugin.load();
this.pluginManager.initializePlugin(this.plugin, data.identifier);
this.plugin = await this.pluginManager.loadPlugin(data.pluginPath);
await this.plugin.load();
await this.pluginManager.initializePlugin(this.plugin, data.identifier);

// change process title to include plugin name
process.title = `homebridge: ${this.plugin.getPluginIdentifier()}`;
Expand Down
13 changes: 9 additions & 4 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { PackageJSON, PluginManager } from "./pluginManager";

const log = Logger.internal;

// Workaround for https://github.com/microsoft/TypeScript/issues/43329
const _importDynamic = new Function("modulePath", "return import(modulePath)");

/**
* Represents a loaded Homebridge plugin.
*/
Expand All @@ -28,6 +31,7 @@ export class Plugin {
private readonly pluginName: PluginName;
private readonly scope?: string; // npm package scope
private readonly pluginPath: string; // like "/usr/local/lib/node_modules/homebridge-lockitron"
private readonly isESM: boolean;

public disabled = false; // mark the plugin as disabled

Expand All @@ -54,6 +58,7 @@ export class Plugin {

this.version = packageJSON.version || "0.0.0";
this.main = packageJSON.main || "./index.js"; // figure out the main module - index.js unless otherwise specified
this.isESM = packageJSON.type === "module";

// very temporary fix for first wave of plugins
if (packageJSON.peerDependencies && (!packageJSON.engines || !packageJSON.engines.homebridge)) {
Expand Down Expand Up @@ -146,7 +151,7 @@ export class Plugin {
return platforms && platforms[0];
}

public load(): void {
public async load(): Promise<void> {
const context = this.loadContext!;
assert(context, "Reached illegal state. Plugin state is undefined!");
this.loadContext = undefined; // free up memory
Expand Down Expand Up @@ -184,7 +189,7 @@ major incompatibility issues and thus is considered bad practice. Please inform

// try to require() it and grab the exported initialization hook
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pluginModules = require(mainPath);
const pluginModules = this.isESM ? await _importDynamic(mainPath) : require(mainPath);

if (typeof pluginModules === "function") {
this.pluginInitializer = pluginModules;
Expand All @@ -195,12 +200,12 @@ major incompatibility issues and thus is considered bad practice. Please inform
}
}

public initialize(api: API): void {
public initialize(api: API): void | Promise<void> {
if (!this.pluginInitializer) {
throw new Error("Tried to initialize a plugin which hasn't been loaded yet!");
}

this.pluginInitializer(api);
return this.pluginInitializer(api);
}


Expand Down
19 changes: 12 additions & 7 deletions src/pluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export interface PackageJSON { // incomplete type for package.json (just stuff w
keywords?: string[];

main?: string;
/**
* When set as module, it marks .js file to be treated as ESM.
* See https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_enabling
*/
type?: "module" | "commonjs";

engines?: Record<string, string>;
dependencies?: Record<string, string>;
Expand Down Expand Up @@ -116,14 +121,14 @@ export class PluginManager {
return identifier.split(".")[0];
}

public initializeInstalledPlugins(): void {
public async initializeInstalledPlugins(): Promise<void> {
log.info("---");

this.loadInstalledPlugins();

this.plugins.forEach((plugin: Plugin, identifier: PluginIdentifier) => {
for(const [identifier, plugin] of this.plugins) {
try {
plugin.load();
await plugin.load();
} catch (error) {
log.error("====================");
log.error(`ERROR LOADING PLUGIN ${identifier}:`);
Expand All @@ -144,18 +149,18 @@ export class PluginManager {
log.info(`Loaded plugin: ${identifier}@${plugin.version}`);
}

this.initializePlugin(plugin, identifier);
await this.initializePlugin(plugin, identifier);

log.info("---");
});
}

this.currentInitializingPlugin = undefined;
}

public initializePlugin(plugin: Plugin, identifier: string): void {
public async initializePlugin(plugin: Plugin, identifier: string): Promise<void> {
try {
this.currentInitializingPlugin = plugin;
plugin.initialize(this.api); // call the plugin's initializer and pass it our API instance
await plugin.initialize(this.api); // call the plugin's initializer and pass it our API instance
} catch (error) {
log.error("====================");
log.error(`ERROR INITIALIZING PLUGIN ${identifier}:`);
Expand Down
2 changes: 1 addition & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class Server {
await this.bridgeService.loadCachedPlatformAccessoriesFromDisk();

// initialize plugins
this.pluginManager.initializeInstalledPlugins();
await this.pluginManager.initializeInstalledPlugins();

if (this.config.platforms.length > 0) {
promises.push(...this.loadPlatforms());
Expand Down

0 comments on commit f26307a

Please sign in to comment.