• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / #15024

13 Dec 2025 02:11AM UTC coverage: 87.29% (-0.5%) from 87.825%
#15024

push

web-flow
Merge d8dcd8d52 into a65ebfcad

14406 of 17439 branches covered (82.61%)

Branch coverage included in aggregate %.

36 of 36 new or added lines in 4 files covered. (100.0%)

909 existing lines in 49 files now uncovered.

15091 of 16353 relevant lines covered (92.28%)

24217.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.67
/src/PluginInterface.ts
1
import type { Plugin } from './interfaces';
2
import { LogLevel, createLogger } from './logging';
1✔
3
import type { Logger } from './logging';
4
/*
5
 * we use `Required` everywhere here because we expect that the methods on plugin objects will
6
 * be optional, and we don't want to deal with `undefined`.
7
 * `extends (...args: any[]) => any` determines whether the thing we're dealing with is a function.
8
 * Returning `never` in the `as` clause of the `[key in object]` step deletes that key from the
9
 * resultant object.
10
 * on the right-hand side of the mapped type we are forced to use a conditional type a second time,
11
 * in order to be able to use the `Parameters` utility type on `Required<T>[K]`. This will always
12
 * be true because of the filtering done by the `[key in object]` clause, but TS requires the duplication.
13
 *
14
 * so put together: we iterate over all of the fields in T, deleting ones which are not (potentially
15
 * optional) functions. For the ones that are, we replace them with their parameters.
16
 *
17
 * this returns the type of an object whose keys are the names of the methods of T and whose values
18
 * are tuples containing the arguments that each method accepts.
19
 */
20
export type PluginEventArgs<T> = {
21
    [K in keyof Required<T> as Required<T>[K] extends (...args: any[]) => any ? K : never]:
22
    Required<T>[K] extends (...args: any[]) => any ? Parameters<Required<T>[K]> : never
23
};
24

25
export default class PluginInterface<T extends Plugin = Plugin> {
1✔
26
    constructor(
27
        plugins?: Plugin[],
28
        options?: {
29
            logger?: Logger;
30
            suppressErrors?: boolean;
31
        }
32
    ) {
33
        this.logger = options?.logger;
2,136✔
34
        this.suppressErrors = (options as any)?.suppressErrors === false ? false : true;
2,136!
35
        for (const plugin of plugins ?? []) {
2,136✔
UNCOV
36
            this.add(plugin);
×
37
        }
38
        if (!this.logger) {
2,136✔
39
            this.logger = createLogger();
1✔
40
        }
41
        if (!this.logger) {
2,136!
UNCOV
42
            this.logger = createLogger();
×
43
        }
44
    }
45

46
    private plugins: Plugin[] = [];
2,136✔
47
    private logger: Logger;
48

49
    /**
50
     * Should plugin errors cause the program to fail, or should they be caught and simply logged
51
     */
52
    private suppressErrors: boolean | undefined;
53

54
    /**
55
     * Call `event` on plugins
56
     */
57
    public emit<K extends keyof PluginEventArgs<T> & string>(event: K, ...args: PluginEventArgs<T>[K]) {
58
        this.logger.debug(`Emitting plugin event: ${event}`);
40,990✔
59
        for (let plugin of this.plugins) {
40,990✔
60
            if ((plugin as any)[event]) {
43,751✔
61
                try {
14,119✔
62
                    this.logger?.time(LogLevel.debug, [plugin.name, event], () => {
14,119!
63
                        (plugin as any)[event](...args);
14,119✔
64
                    });
65
                } catch (err) {
66
                    this.logger?.error(`Error when calling plugin ${plugin.name}.${event}:`, (err as Error).stack);
3!
67
                    if (!this.suppressErrors) {
3!
UNCOV
68
                        throw err;
×
69
                    }
70
                }
71
            }
72
        }
73
        return args[0];
40,990✔
74
    }
75

76
    /**
77
     * Call `event` on plugins, but allow the plugins to return promises that will be awaited before the next plugin is notified
78
     */
79
    public async emitAsync<K extends keyof PluginEventArgs<T> & string>(event: K, ...args: PluginEventArgs<T>[K]): Promise<PluginEventArgs<T>[K][0]> {
80
        this.logger.debug(`Emitting async plugin event: ${event}`);
11,883✔
81
        for (let plugin of this.plugins) {
11,883✔
82
            if ((plugin as any)[event]) {
22,809✔
83
                try {
4,301✔
84
                    await this.logger?.time(LogLevel.debug, [plugin.name, event], async () => {
4,301!
85
                        await Promise.resolve(
4,301✔
86
                            (plugin as any)[event](...args)
87
                        );
88
                    });
89
                } catch (err) {
90
                    this.logger?.error(`Error when calling plugin ${plugin.name}.${event}:`, (err as Error).stack);
2!
91
                    if (!this.suppressErrors) {
2!
UNCOV
92
                        throw err;
×
93
                    }
94
                }
95
            }
96
        }
97
        return args[0];
11,883✔
98
    }
99

100
    /**
101
     * Add a plugin to the beginning of the list of plugins
102
     */
103
    public addFirst<T extends Plugin = Plugin>(plugin: T) {
104
        if (!this.has(plugin)) {
2,465!
105
            this.plugins.unshift(plugin);
2,465✔
106
        }
107
        return plugin;
2,465✔
108
    }
109

110
    /**
111
     * Add a plugin to the end of the list of plugins
112
     */
113
    public add<T extends Plugin = Plugin>(plugin: T) {
114
        if (!this.has(plugin)) {
162✔
115
            this.sanitizePlugin(plugin);
161✔
116
            this.plugins.push(plugin);
161✔
117
        }
118
        return plugin;
162✔
119
    }
120

121
    /**
122
     * Find deprecated or removed historic plugin hooks, and warn about them.
123
     * Some events can be forwards-converted
124
     */
125
    private sanitizePlugin(plugin: Plugin) {
126
        const removedHooks = [
161✔
127
            'beforePrepublish',
128
            'afterPrepublish'
129
        ];
130
        for (const removedHook of removedHooks) {
161✔
131
            if (plugin[removedHook]) {
322!
UNCOV
132
                this.logger?.error(`Plugin "${plugin.name}": event ${removedHook} is no longer supported and will never be called`);
×
133
            }
134
        }
135

136
        const upgradeWithWarn = {
161✔
137
            beforePublish: 'beforeSerializeProgram',
138
            afterPublish: 'afterSerializeProgram',
139
            beforeProgramTranspile: 'beforeBuildProgram',
140
            afterProgramTranspile: 'afterBuildProgram',
141
            beforeFileParse: 'beforeProvideFile',
142
            afterFileParse: 'afterProvideFile',
143
            beforeFileTranspile: 'beforePrepareFile',
144
            afterFileTranspile: 'afterPrepareFile',
145
            beforeFileDispose: 'beforeFileRemove',
146
            afterFileDispose: 'afterFileRemove'
147
        };
148

149
        for (const [oldEvent, newEvent] of Object.entries(upgradeWithWarn)) {
161✔
150
            if (plugin[oldEvent]) {
1,610✔
151
                if (!plugin[newEvent]) {
7!
152
                    plugin[newEvent] = plugin[oldEvent];
7✔
153
                    this.logger?.warn(`Plugin '${plugin.name}': event '${oldEvent}' is no longer supported. It has been converted to '${newEvent}' but you may encounter issues as their signatures do not match.`);
7!
154
                } else {
UNCOV
155
                    this.logger?.warn(`Plugin "${plugin.name}": event '${oldEvent}' is no longer supported and will never be called`);
×
156
                }
157
            }
158
        }
159
    }
160

161
    public has(plugin: Plugin) {
162
        return this.plugins.includes(plugin);
2,982✔
163
    }
164

165
    public remove<T extends Plugin = Plugin>(plugin: T) {
166
        if (this.has(plugin)) {
352!
167
            this.plugins.splice(this.plugins.indexOf(plugin), 1);
352✔
168
        }
169
        return plugin;
352✔
170
    }
171

172
    /**
173
     * Remove all plugins
174
     */
175
    public clear() {
UNCOV
176
        this.plugins = [];
×
177
    }
178
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc