The brighterscript compiler is extensible using JavaScript plugins.
NOTE: The plugin system is currently in alpha. It's brand new, and while we hope there won't be any significant changes to the API, we reserve the right to change things before moving the plugin system out of alpha.
You can refer to node scripts / npm modules directly in the bsconfig.json.
Those plugins will be loaded by the VSCode extension and can provide live diagnostics within the editor.
{
"plugins": [
"./scripts/myPlugin.js",
"@rokucommunity/bslint"
]
}npx bsc --plugins "./scripts/myPlugin.js" "@rokucommunity/bslint"When using the compiler API directly, plugins can directly reference your code:
import { ProgramBuilder } from 'brighterscript';
import myPlugin from './myPlugin';
const builder = new ProgramBuilder();
builder.addPlugin(myPlugin);
builder.run(/*...*/);While there are no restrictions on plugin names, it helps others to find your plugin on npm when you follow these naming conventions:
- Unscoped: If your npm package name won’t be scoped (doesn’t begin with
@), then the plugin name should begin withbsc-plugin-, such asbsc-plugin-auto-findnode. - Scoped: If your npm package name will be scoped, then the plugin name should be in the format of
@<scope>/bsc-plugin-<plugin-name>such as@rokucommunity/bsc-plugin-auto-findnodeor even@<scope>/bsc-pluginsuch as@maestro/bsc-plugin.
Full compiler lifecycle:
beforeProgramCreateafterProgramCreateafterScopeCreate("source" scope)- For each file:
beforeFileParseafterFileParseafterScopeCreate(component scope)
beforeProgramValidate- For each file:
beforeFileValidateonFileValidateafterFileValidate
- For each scope:
beforeScopeValidateonScopeValidateafterScopeValidate
afterProgramValidate
beforePrepublishafterPrepublishbeforePublishbeforeProgramTranspile- For each file:
beforeFileTranspileafterFileTranspile
afterProgramTranspile
afterPublishbeforeProgramDispose
Once the program has been validated, the language server runs a special loop - it never reaches the publishing steps.
When a file is removed:
beforeFileDisposebeforeScopeDispose(component scope)afterScopeDispose(component scope)afterFileDispose
When a file is added:
beforeFileParseafterFileParseafterScopeCreate(component scope)afterFileValidate
When any file is modified:
- Remove file
- Add file
After file addition/removal (note: throttled/debounced):
beforeProgramValidate- For each invalidated/not-yet-validated file
beforeFileValidateonFileValidateafterFileValidate
- For each invalidated scope:
beforeScopeValidateafterScopeValidate
afterProgramValidate
Code Actions
onGetCodeActions
Completions
beforeProvideCompletionsprovideCompletionsafterProvideCompletions
Hovers
beforeProvideHoverprovideHoverafterProvideHover
Semantic Tokens
onGetSemanticTokens
Objects in the brighterscript compiler dispatch a number of events that you can listen to. These events allow you to peek / modify the objects that the compiler manipulates.
The top level object is the ProgramBuilder which runs the overall process: preparation, running the compiler (Program) and producing the output.
Program is the compiler itself; it handles the creation of program objects (Scope, BrsFile, XmlFile) and overal validation.
BrsFile is a .brs or .bs file; its AST can be modified by a plugin.
XmlFile is a component; it doesn't have a proper AST currently but it can be "patched".
Scope is a logical group of files; for instance source files, or each component's individual scope (XML definition, related components and imported scripts). A file can be included in multiple scopes.
Files are superficially validated after parsing, then re-validated as part of the scope they are included in.
Here are some important interfaces. You can view them in the code at this link.
export type CompilerPluginFactory = (options?: PluginFactoryOptions) => CompilierPlugin;
export interface CompilerPlugin {
name: string;
//program events
beforeProgramCreate?: (builder: ProgramBuilder) => void;
beforePrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void;
afterPrepublish?: (builder: ProgramBuilder, files: FileObj[]) => void;
beforePublish?: (builder: ProgramBuilder, files: FileObj[]) => void;
afterPublish?: (builder: ProgramBuilder, files: FileObj[]) => void;
afterProgramCreate?: (program: Program) => void;
beforeProgramValidate?: (program: Program) => void;
afterProgramValidate?: (program: Program) => void;
beforeProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void;
afterProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void;
beforeProgramDispose?: PluginHandler<BeforeProgramDisposeEvent>;
onGetCodeActions?: PluginHandler<OnGetCodeActionsEvent>;
/**
* Emitted before the program starts collecting completions
*/
beforeProvideCompletions?: PluginHandler<BeforeProvideCompletionsEvent>;
/**
* Use this event to contribute completions
*/
provideCompletions?: PluginHandler<ProvideCompletionsEvent>;
/**
* Emitted after the program has finished collecting completions, but before they are sent to the client
*/
afterProvideCompletions?: PluginHandler<AfterProvideCompletionsEvent>;
/**
* Called before the `provideHover` hook. Use this if you need to prepare any of the in-memory objects before the `provideHover` gets called
*/
beforeProvideHover?: PluginHandler<BeforeProvideHoverEvent>;
/**
* Called when bsc looks for hover information. Use this if your plugin wants to contribute hover information.
*/
provideHover?: PluginHandler<ProvideHoverEvent>;
/**
* Called after the `provideHover` hook. Use this if you want to intercept or sanitize the hover data (even from other plugins) before it gets sent to the client.
*/
afterProvideHover?: PluginHandler<AfterProvideHoverEvent>;
/**
* Called before the `provideDefinition` hook
*/
beforeProvideDefinition?(event: BeforeProvideDefinitionEvent): any;
/**
* Provide one or more `Location`s where the symbol at the given position was originally defined
* @param event
*/
provideDefinition?(event: ProvideDefinitionEvent): any;
/**
* Called after `provideDefinition`. Use this if you want to intercept or sanitize the definition data provided by bsc or other plugins
* @param event
*/
afterProvideDefinition?(event: AfterProvideDefinitionEvent): any;
/**
* Called before the `provideReferences` hook
*/
beforeProvideReferences?(event: BeforeProvideReferencesEvent): any;
/**
* Provide all of the `Location`s where the symbol at the given position is located
* @param event
*/
provideReferences?(event: ProvideReferencesEvent): any;
/**
* Called after `provideReferences`. Use this if you want to intercept or sanitize the references data provided by bsc or other plugins
* @param event
*/
afterProvideReferences?(event: AfterProvideReferencesEvent): any;
/**
* Called before the `provideDocumentSymbols` hook
*/
beforeProvideDocumentSymbols?(event: BeforeProvideDocumentSymbolsEvent): any;
/**
* Provide all of the `DocumentSymbol`s for the given file
* @param event
*/
provideDocumentSymbols?(event: ProvideDocumentSymbolsEvent): any;
/**
* Called after `provideDocumentSymbols`. Use this if you want to intercept or sanitize the document symbols data provided by bsc or other plugins
* @param event
*/
afterProvideDocumentSymbols?(event: AfterProvideDocumentSymbolsEvent): any;
/**
* Called before the `provideWorkspaceSymbols` hook
*/
beforeProvideWorkspaceSymbols?(event: BeforeProvideWorkspaceSymbolsEvent): any;
/**
* Provide all of the workspace symbols for the entire project
* @param event
*/
provideWorkspaceSymbols?(event: ProvideWorkspaceSymbolsEvent): any;
/**
* Called after `provideWorkspaceSymbols`. Use this if you want to intercept or sanitize the workspace symbols data provided by bsc or other plugins
* @param event
*/
afterProvideWorkspaceSymbols?(event: AfterProvideWorkspaceSymbolsEvent): any;
onGetSemanticTokens?: PluginHandler<OnGetSemanticTokensEvent>;
//scope events
afterScopeCreate?: (scope: Scope) => void;
beforeScopeDispose?: (scope: Scope) => void;
afterScopeDispose?: (scope: Scope) => void;
beforeScopeValidate?: ValidateHandler;
onScopeValidate?: PluginHandler<OnScopeValidateEvent>;
afterScopeValidate?: ValidateHandler;
//file events
beforeFileParse?: (source: SourceObj) => void;
afterFileParse?: (file: BscFile) => void;
/**
* Called before each file is validated
*/
beforeFileValidate?: PluginHandler<BeforeFileValidateEvent>;
/**
* Called during the file validation process. If your plugin contributes file validations, this is a good place to contribute them.
*/
onFileValidate?: PluginHandler<OnFileValidateEvent>;
/**
* Called after each file is validated
*/
afterFileValidate?: (file: BscFile) => void;
beforeFileTranspile?: PluginHandler<BeforeFileTranspileEvent>;
afterFileTranspile?: PluginHandler<AfterFileTranspileEvent>;
beforeFileDispose?: (file: BscFile) => void;
afterFileDispose?: (file: BscFile) => void;
}
// related types:
interface FileObj {
src: string;
dest: string;
}
interface SourceObj {
pathAbsolute: string;
source: string;
}
interface TranspileObj {
file: (BscFile);
outputPath: string;
}
type ValidateHandler = (scope: Scope, files: BscFile[], callables: CallableContainerMap) => void;
interface CallableContainerMap {
[name: string]: CallableContainer[];
}
interface CallableContainer {
callable: Callable;
scope: Scope;
}To register a plugin, you need to have a default export that returns a factory. BrighterScript will call this factory once for every instance of a program that needs the plugin. This means that you might have multiple instances of a plugin in one NodeJS process (one for each brighterscript project).
export default function () {
return {
name: 'Name of your plugin'
};
}There is currently a v1 rewrite of BrighterScript which includes some significant breaking changes. This has caused challenges with brighterscript plugins, as you need to maintain separate versions for v0 and v1. You can utilize the pluginOptions.version property in your factory to determine what brighterscript version is running your plugin, and adjust accordingly.
This options object is only available starting with brighterscript v0.69.4 and v1.0.0-alpha.45. It's important to support backwards compatibility that an undefined version value be treated as a v0 plugin.
Here's a small example:
export default function (pluginOptions: PluginOptions) {
if(semver.major(pluginOptions?.version) === '1') {
return {
name: 'v1 version of your plugin!'
};
} else {
return {
name: 'v0 version of your plugin'
};
}
}Plugins are JavaScript modules dynamically loaded by the compiler. Their entry point is a default function that returns an object that follows the CompilerPlugin interface.
To walk/modify the AST, a number of helpers are provided in brighterscript/dist/parser/ASTUtils.
It is highly recommended to use TypeScript as intellisense helps greatly writing code against new APIs.
The ts-node module can transpile typescript on the fly. This is by far the easiest way to develop a brighterscript plugin, as you can elimintate the manual typescript transpile step.
# install modules needed to compile a plugin
npm install brighterscript typescript @types/node ts-node -D
#run the brighterscript cli and use ts-node to dynamically transpile your plugin
npx bsc --sourceMap --require ts-node/register --plugins myPlugin.tsThe require flag can also be set in the bsconfig.json, simplifying your bsc command arguments.
{
"require": ["ts-node/register"]
}If you would prefer to transpile your plugin manually, you can follow these steps:
# install modules needed to compile a plugin
npm install brighterscript typescript @types/node -D
# transpile to JS (with source maps for debugging)
npx tsc myPlugin.ts -m commonjs --sourceMap
# add --watch for continuous transpilation
npx tsc myPlugin.ts -m commonjs --sourceMap --watchDiagnostics must run every time it is relevant:
- Files have one out-of-context validation step (
afterFileValidate), - Scopes are validated once all the the files have been collected (
before/afterScopeValidate).
Note: in a language-server context, Scope validation happens every time a file changes.
// bsc-plugin-no-underscores.ts
import { CompilerPlugin, BscFile, isBrsFile } from 'brighterscript';
// plugin factory
export default function () {
return {
name: 'no-underscores',
// post-parsing validation
afterFileValidate: (file: BscFile) => {
if (isBrsFile(file)) {
// visit function statements and validate their name
file.parser.references.functionStatements.forEach((fun) => {
if (fun.name.text.includes('_')) {
file.addDiagnostics([{
code: 9000,
message: 'Do not use underscores in function names',
range: fun.name.range,
file
}]);
}
});
}
}
} as CompilerPlugin;
};Sometimes plugins will want to modify code before the project is transpiled. While you can technically edit the AST directly at any point in the file's lifecycle, this is not recommended as those changes will remain changed as long as that file exists in memory and could cause issues with file validation if the plugin is used in a language-server context (i.e. inside vscode).
Instead, we provide an instace of an AstEditor class in the beforeFileTranspile event that allows you to modify AST before the file is transpiled, and then those modifications are undone afterFileTranspile.
For example, consider the following brightscript code:
sub main()
print "hello <FIRST_NAME>"
end subHere's the plugin:
import { CompilerPlugin, BeforeFileTranspileEvent, isBrsFile, WalkMode, createVisitor, TokenKind } from 'brighterscript';
// plugin factory
export default function () {
return {
name: 'replacePlaceholders',
// transform AST before transpilation
beforeFileTranspile: (event: BeforeFileTranspileEvent) => {
if (isBrsFile(event.file)) {
event.file.ast.walk(createVisitor({
LiteralExpression: (literal) => {
//replace every occurance of <FIRST_NAME> in strings with "world"
if (literal.token.kind === TokenKind.StringLiteral && literal.token.text.includes('<FIRST_NAME>')) {
event.editor.setProperty(literal.token, 'text', literal.token.text.replace('<FIRST_NAME>', 'world'));
}
}
}), {
walkMode: WalkMode.visitExpressionsRecursive
});
}
}
} as CompilerPlugin;
};This plugin will search through every LiteralExpression in the entire project, and every time we find a string literal, we will replace <FIRST_NAME> with world. This is done with the event.editor object. editor allows you to apply edits to the AST, and then the brighterscript compiler will undo those edits once the file has been transpiled.
Another common use case is to remove print statements and comments. Here's a plugin to do that:
import { isBrsFile, createVisitor, WalkMode, BeforeFileTranspileEvent, CompilerPlugin } from 'brighterscript';
export default function plugin() {
return {
name: 'removeCommentAndPrintStatements',
beforeFileTranspile: (event: BeforeFileTranspileEvent) => {
if (isBrsFile(event.file)) {
// visit functions bodies and replace `PrintStatement` nodes with `EmptyStatement`
for (const func of event.file.parser.references.functionExpressions) {
func.body.walk(createVisitor({
PrintStatement: (statement) => {
event.editor.overrideTranspileResult(statement, '');
},
CommentStatement: (statement) => {
event.editor.overrideTranspileResult(statement, '');
}
}), {
walkMode: WalkMode.visitStatements
});
}
}
}
} as CompilerPlugin;
}In some cases you may want to modify the project's configuration via a plugin, such as to change settings based on environment variables or to dynamically modify the project's files array. Plugins may do so in the beforeProgramCreate step. For example, here's a plugin which adds an additional file to the build:
import { CompilerPlugin, ProgramBuilder } from 'brighterscript';
export default function plugin() {
return {
name: 'addFilesDynamically',
beforeProgramCreate: (builder: ProgramBuilder) => {
if (!builder.options.files) {
builder.options.files = [];
}
builder.options.files.push({
src: "path/to/plugin/file.bs",
dest: "some/destination/path/goes/here"
})
}
} as CompilerPlugin;
}