Skip to content
Merged
138 changes: 68 additions & 70 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,57 +50,55 @@ While there are no restrictions on plugin names, it helps others to find your pl

Full compiler lifecycle:

- `beforeProgramCreate`
- `afterProgramCreate`
- `afterScopeCreate` ("source" scope)
- `beforeProvideProgram`
- `afterProvideProgram`
- `afterProvideScope` ("source" scope)
- For each physical file:
- `beforeProvideFile`
- `onProvideFile`
- `afterProvideFile`
- `beforeFileParse` (deprecated)
- `afterFileParse` (deprecated)
- For each physical and virtual file
- `beforeAddFile`
- `afterAddFile`
- `afterScopeCreate` (component scope)
- `beforeProgramValidate`
- `afterProvideScope` (component scope)
- `beforeValidateProgram`
- For each file:
- `beforeFileValidate`
- `onFileValidate`
- `afterFileValidate`
- `beforeValidateFile`
- `validateFile`
- `afterValidateFile`
- For each scope:
- `beforeScopeValidate`
- `onScopeValidate`
- `afterScopeValidate`
- `afterProgramValidate`
- `beforeValidateScope`
- `validateScope`
- `afterValidateScope`
- `afterValidateProgram`
- `beforePrepublish`
- `afterPrepublish`
- `beforePublish`
- `beforeProgramTranspile`
- `beforeSerializeProgram`
- `beforeBuildProgram`
- For each file:
- `beforeFileTranspile`
- `afterFileTranspile`
- `afterProgramTranspile`
- `afterPublish`
- `beforeProgramDispose`
- `beforePrepareFile`
- `afterPrepareFile`
- `afterBuildProgram`
- `afterSerializeProgram`
- `beforeRemoveProgram`

### Language server

Once the program has been validated, the language server runs a special loop - it never reaches the publishing steps.

When a file is removed:

- `beforeFileDispose`
- `beforeScopeDispose` (component scope)
- `afterScopeDispose` (component scope)
- `afterFileDispose`
- `beforeRemoveFile`
- `beforeRemoveScope` (component scope)
- `afterRemoveScope` (component scope)
- `afterRemoveFile`

When a file is added:

- `beforeFileParse`
- `afterFileParse`
- `afterScopeCreate` (component scope)
- `afterFileValidate`
- `beforeProvideFile`
- `afterProvideFile`
- `afterProvideScope` (component scope)
- `afterValidateFile`

When any file is modified:

Expand All @@ -109,18 +107,18 @@ When any file is modified:

After file addition/removal (note: throttled/debounced):

- `beforeProgramValidate`
- `beforeValidateProgram`
- For each invalidated/not-yet-validated file
- `beforeFileValidate`
- `onFileValidate`
- `afterFileValidate`
- `beforeValidateFile`
- `validateFile`
- `afterValidateFile`
- For each invalidated scope:
- `beforeScopeValidate`
- `afterScopeValidate`
- `afterProgramValidate`
- `beforeValidateScope`
- `afterValidateScope`
- `afterValidateProgram`

Code Actions
- `onGetCodeActions`
- `provideCodeActions`

Completions
- `beforeProvideCompletions`
Expand All @@ -133,7 +131,7 @@ Hovers
- `afterProvideHover`

Semantic Tokens
- `onGetSemanticTokens`
- `provideSemanticTokens`


## Compiler API
Expand Down Expand Up @@ -164,11 +162,11 @@ export interface CompilerPlugin {
/**
* Called before a new program is created
*/
beforeProgramCreate?(event: BeforeProgramCreateEvent): any;
beforeProvideProgram?(event: BeforeProvideProgramEvent): any;
/**
* Called after a new program is created
*/
afterProgramCreate?(event: AfterProgramCreateEvent): any;
afterProvideProgram?(event: AfterProvideProgramEvent): any;


/**
Expand All @@ -188,20 +186,20 @@ export interface CompilerPlugin {
/**
* Called before the entire program is validated
*/
beforeProgramValidate?(event: BeforeProgramValidateEvent): any;
beforeValidateProgram?(event: BeforeValidateProgramEvent): any;
/**
* Called before the entire program is validated
*/
onProgramValidate?(event: OnProgramValidateEvent): any;
validateProgram?(event: ValidateProgramEvent): any;
/**
* Called after the program has been validated
*/
afterProgramValidate?(event: AfterProgramValidateEvent): any;
afterValidateProgram?(event: AfterValidateProgramEvent): any;

/**
* Called right before the program is disposed/destroyed
*/
beforeProgramDispose?(event: BeforeProgramDisposeEvent): any;
beforeRemoveProgram?(event: BeforeRemoveProgramEvent): any;

/**
* Emitted before the program starts collecting completions
Expand Down Expand Up @@ -233,13 +231,13 @@ export interface CompilerPlugin {
/**
* Called after a scope was created
*/
afterScopeCreate?(event: AfterScopeCreateEvent): any;
afterProvideScope?(event: AfterProvideScopeEvent): any;

beforeScopeDispose?(event: BeforeScopeDisposeEvent): any;
onScopeDispose?(event: OnScopeDisposeEvent): any;
afterScopeDispose?(event: AfterScopeDisposeEvent): any;
beforeRemoveScope?(event: BeforeRemoveProgramEvent): any;
removeScope?(event: RemoveScopeEvent): any;
afterRemoveScope?(event: AfterRemoveScopeEvent): any;

beforeScopeValidate?(event: BeforeScopeValidateEvent): any;
beforeValidateScope?(event: BeforeValidateScopeEvent): any;

/**
* Called before the `provideDefinition` hook
Expand Down Expand Up @@ -305,13 +303,13 @@ export interface CompilerPlugin {
afterProvideWorkspaceSymbols?(event: AfterProvideWorkspaceSymbolsEvent): any;


onGetSemanticTokens?: PluginHandler<OnGetSemanticTokensEvent>;
provideSemanticTokens?: PluginHandler<ProvideSemanticTokensEvent>;
//scope events
onScopeValidate?(event: OnScopeValidateEvent): any;
afterScopeValidate?(event: BeforeScopeValidateEvent): any;
validateScope?(event: ValidateScopeEvent): any;
afterValidateScope?(event: BeforeValidateScopeEvent): any;

onGetCodeActions?(event: OnGetCodeActionsEvent): any;
onGetSemanticTokens?(event: OnGetSemanticTokensEvent): any;
provideCodeActions?(event: ProvideCodeActionsEvent): any;
provideSemanticTokens?(event: ProvideSemanticTokensEvent): any;


/**
Expand All @@ -333,35 +331,35 @@ export interface CompilerPlugin {
* Called before a file is added to the program.
* Includes physical files as well as any virtual files produced by `provideFile` events
*/
beforeFileAdd?(event: BeforeFileAddEvent): any;
beforeAddFile?(event: BeforeAddFileEvent): any;
/**
* Called after a file has been added to the program.
* Includes physical files as well as any virtual files produced by `provideFile` events
*/
afterFileAdd?(event: AfterFileAddEvent): any;
afterAddFile?(event: AfterAddFileEvent): any;

/**
* Called before a file is removed from the program. This includes physical and virtual files
*/
beforeFileRemove?(event: BeforeFileRemoveEvent): any;
beforeRemoveFile?(event: BeforeRemoveFileEvent): any;
/**
* Called after a file has been removed from the program. This includes physical and virtual files
*/
afterFileRemove?(event: AfterFileRemoveEvent): any;
afterRemoveFile?(event: AfterRemoveFileEvent): any;


/**
* Called before each file is validated
*/
beforeFileValidate?(event: BeforeFileValidateEvent): any;
beforeValidateFile?(event: BeforeValidateFileEvent): any;
/**
* Called during the file validation process. If your plugin contributes file validations, this is a good place to contribute them.
*/
onFileValidate?(event: OnFileValidateEvent): any;
validateFile?(event: ValidateFileEvent): any;
/**
* Called after each file is validated
*/
afterFileValidate?(event: AfterFileValidateEvent): any;
afterValidateFile?(event: AfterValidateFileEvent): any;


/**
Expand Down Expand Up @@ -395,7 +393,7 @@ export interface CompilerPlugin {
/**
* Emitted right at the start of the program turning all file objects into their final buffers
*/
onSerializeProgram?(event: OnSerializeProgramEvent): any;
serializeProgram?(event: SerializeProgramEvent): any;
/**
* After the program turns all file objects into their final buffers
*/
Expand Down Expand Up @@ -542,8 +540,8 @@ npx tsc myPlugin.ts -m commonjs --sourceMap --watch

Diagnostics 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`).
- Files have one out-of-context validation step (`afterValidateFile`),
- Scopes are validated once all the the files have been collected (`before/afterValidateScope`).

Note: in a language-server context, Scope validation happens every time a file changes.

Expand All @@ -558,7 +556,7 @@ export default function () {
return {
name: 'no-underscores',
// post-parsing validation
afterFileValidate: (event: AfterFileValidateEvent) => {
afterValidateFile: (event: AfterValidateFileEvent) => {
if (isBrsFile(event.file)) {
// clear existing diagnostics
event.program.diagnosticManager.clearByContext({file: file, tag: 'no-underscores-plugin'});
Expand Down Expand Up @@ -587,7 +585,7 @@ export default function () {
## Modifying code
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 `Editor` class in the `beforeFileTranspile` event that allows you to modify AST before the file is transpiled, and then those modifications are undone `afterFileTranspile`.
Instead, we provide an instace of an `Editor` class in the `beforePrepareFile` event that allows you to modify AST before the file is transpiled, and then those modifications are undone `afterPrepareFile`.

For example, consider the following brightscript code:
```brightscript
Expand All @@ -606,7 +604,7 @@ export default function () {
return {
name: 'replacePlaceholders',
// transform AST before transpilation
beforeFileTranspile: (event: BeforeFileTranspileEvent) => {
beforePrepareFile: (event: BeforeFileTranspileEvent) => {
if (isBrsFile(event.file)) {
event.file.ast.walk(createVisitor({
LiteralExpression: (literal) => {
Expand Down Expand Up @@ -670,14 +668,14 @@ export default function (): Plugin {

## Modifying `bsconfig.json` via a plugin

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:
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 `beforeProvideProgram` step. For example, here's a plugin which adds an additional file to the build:
```typescript
import { CompilerPlugin, ProgramBuilder } from 'brighterscript';

export default function plugin() {
return {
name: 'addFilesDynamically',
beforeProgramCreate: (builder: ProgramBuilder) => {
beforeProvideProgram: (builder: ProgramBuilder) => {
if (!builder.options.files) {
builder.options.files = [];
}
Expand Down Expand Up @@ -791,7 +789,7 @@ You can see examples of this in the previous code snippets above.
### Program changes
Historically, only `.brs`, `.bs`, and `.xml` files would be present in the `Program`. As a result of the File API being introduced, now all files included as a result of the bsconfig.json `files` array will be present in the program. Unhandled files will be loaded as generic `AssetFile` instances. This may impact plugins that aren't properly guarding against specific file types. Consider this plugin code:
```typescript
onFileValidate(event){
validateFile(event){
if (isXmlFile(event.file)) {
// do XmlFile work
} else {
Expand Down
6 changes: 3 additions & 3 deletions src/LanguageServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1723,7 +1723,7 @@ describe('LanguageServer', () => {
expect(result['pathAbsolute']).to.eql(result.srcPath);
});

it('calls beforeProgramTranspile and afterProgramTranspile plugin events', async () => {
it('calls beforeBuildProgram and afterBuildProgram plugin events', async () => {
fsExtra.outputFileSync(s`${rootDir}/source/main.bs`, `
sub main()
print \`hello world\`
Expand All @@ -1736,7 +1736,7 @@ describe('LanguageServer', () => {
//make a plugin that changes string text
(server['projectManager'].projects[0] as Project)['builder'].program.plugins.add({
name: 'test-plugin',
beforeProgramTranspile: (event) => {
beforeBuildProgram: (event) => {
const { program, editor } = event;
const file = program.getFile('source/main.bs');
if (isBrsFile(file)) {
Expand All @@ -1751,7 +1751,7 @@ describe('LanguageServer', () => {
});
}
},
afterProgramTranspile: afterSpy
afterBuildProgram: afterSpy
});

const result = (await server.onExecuteCommand({
Expand Down
28 changes: 25 additions & 3 deletions src/PluginInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,37 @@ export default class PluginInterface<T extends Plugin = Plugin> {
afterFileParse: 'afterProvideFile',
beforeFileTranspile: 'beforePrepareFile',
afterFileTranspile: 'afterPrepareFile',
beforeFileDispose: 'beforeFileRemove',
afterFileDispose: 'afterFileRemove'
beforeFileDispose: 'beforeRemoveFile',
afterFileDispose: 'afterRemoveFile',
beforeProgramCreate: 'beforeProvideProgram',
afterProgramCreate: 'afterProvideProgram',
beforeProgramValidate: 'beforeValidateProgram',
onProgramValidate: 'validateProgram',
beforeProgramDispose: 'beforeRemoveProgram',
afterScopeCreate: 'afterProvideScope',
beforeScopeDispose: 'beforeRemoveScope',
onScopeDispose: 'removeScope',
afterScopeDispose: 'afterRemoveScope',
beforeScopeValidate: 'beforeValidateScope',
onScopeValidate: 'validateScope',
afterScopeValidate: 'afterValidateScope',
onGetCodeActions: 'provideCodeActions',
onGetSemanticTokens: 'provideSemanticTokens',
beforeFileAdd: 'beforeAddFile',
afterFileAdd: 'afterAddFile',
beforeFileRemove: 'beforeRemoveFile',
afterFileRemove: 'afterRemoveFile',
beforeFileValidate: 'beforeValidateFile',
onFileValidate: 'validateFile',
afterFileValidate: 'afterValidateFile',
onSerializeProgram: 'serializeProgram'
};

for (const [oldEvent, newEvent] of Object.entries(upgradeWithWarn)) {
if (plugin[oldEvent]) {
if (!plugin[newEvent]) {
plugin[newEvent] = plugin[oldEvent];
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.`);
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 may not match.`);
} else {
this.logger?.warn(`Plugin "${plugin.name}": event '${oldEvent}' is no longer supported and will never be called`);
}
Expand Down
Loading