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

File api #408

Merged
merged 68 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
1327541
More flexible cache typing
TwitchBronBron Apr 28, 2022
9fe2c13
Better typing for cache `get`
TwitchBronBron Apr 28, 2022
d710cff
Refactor files to use `srcPath`.
TwitchBronBron May 3, 2022
8b00b81
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron May 3, 2022
eaa7026
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron May 6, 2022
bb0ff64
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Sep 13, 2022
0598dd7
WIP
TwitchBronBron Sep 13, 2022
1b57e6e
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Sep 14, 2022
4b09dae
Add start of unit test fixes
TwitchBronBron Sep 20, 2022
b4bab33
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Sep 20, 2022
54bfdca
Fix broken scope diagnostics
TwitchBronBron Sep 20, 2022
6adcd56
Fix most tests.
TwitchBronBron Sep 20, 2022
dea96c5
Add support for `RawFile`
TwitchBronBron Sep 20, 2022
0d69f28
Fix remaining broken test
TwitchBronBron Sep 20, 2022
59bbf89
Fix lint issues
TwitchBronBron Sep 20, 2022
cf0c928
Rename `RawFile` to `GenericFile`.
TwitchBronBron Sep 20, 2022
311beac
Rename GenericFile to AssetFile
TwitchBronBron Sep 21, 2022
e984200
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Sep 21, 2022
5b6da0a
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Sep 21, 2022
4f52fee
Add before/after file add events.
TwitchBronBron Sep 22, 2022
56f5129
Allows diagnostics for AssetFiles
TwitchBronBron Sep 22, 2022
9f69422
add srcExtension to provideFile event
TwitchBronBron Sep 23, 2022
c4c53b1
Add basic example to plugin docs
TwitchBronBron Sep 23, 2022
ae55d22
Add `excludeFromOutput` option to files.
TwitchBronBron Sep 23, 2022
479cc98
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Sep 29, 2022
4dce9f1
Rename destPath to pkgPath
TwitchBronBron Sep 29, 2022
39abf33
Implement file factory
TwitchBronBron Sep 29, 2022
b3fd80a
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Sep 30, 2022
9f30345
fix lint errors
TwitchBronBron Sep 30, 2022
6a6241f
before/after file remove events.
TwitchBronBron Sep 30, 2022
2628f21
better plugin docs
TwitchBronBron Sep 30, 2022
3aca36d
Deprecate the `BscFile` interface
TwitchBronBron Sep 30, 2022
1868e70
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Sep 30, 2022
36eb5f3
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Oct 3, 2022
c0e195a
Introduce `destPath` (not using it fully yet)
TwitchBronBron Nov 18, 2022
c7a594a
improve program.getPaths to support destPath
TwitchBronBron Nov 18, 2022
344abe7
Better file constructor handling.
TwitchBronBron Nov 18, 2022
69d0e3f
Use `destPath` instead of `pkgPath` most places
TwitchBronBron Nov 22, 2022
22070ee
Rename file reference to use `destPath`
TwitchBronBron Nov 22, 2022
7001a3a
add file api documentation
TwitchBronBron Nov 22, 2022
d5c6f1e
add multi-file change docs
TwitchBronBron Nov 22, 2022
25ac25b
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Nov 23, 2022
f711b27
Refine file api docs
TwitchBronBron Nov 23, 2022
5b55970
Move bsc plugin to provide files in after
TwitchBronBron Nov 23, 2022
45499fb
Adds completed file build flow
TwitchBronBron Dec 6, 2022
f6054c9
Add program build events
TwitchBronBron Dec 6, 2022
e76cc60
Some fixes
TwitchBronBron Dec 7, 2022
ad15d24
Fix remaining build errors.
TwitchBronBron Dec 7, 2022
fdcda83
Convert testTranspile to async
TwitchBronBron Dec 7, 2022
5c946fc
Convert unit tests to async testTranspile
TwitchBronBron Dec 12, 2022
a67f9d7
Fix transpile flows
TwitchBronBron Dec 13, 2022
b7f440b
Fix lint issues
TwitchBronBron Dec 13, 2022
810cfcf
Fix AssetFile content loading
TwitchBronBron Dec 13, 2022
945160c
Fixed stagingDir path issues
TwitchBronBron Dec 13, 2022
d16aab6
Fix binary file copying
TwitchBronBron Dec 16, 2022
31da76d
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Dec 16, 2022
7ba81b5
Re-expose concrete AstEditor
TwitchBronBron Dec 16, 2022
02f160c
Fix bugs in the bslib injector
TwitchBronBron Dec 16, 2022
bf36c8d
Properly prefix bslib
TwitchBronBron Dec 19, 2022
a61c572
Export some missing interfaces and classes
TwitchBronBron Dec 20, 2022
324c623
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Dec 20, 2022
3d0f2bf
Public `needsTranspiled` props
TwitchBronBron Dec 21, 2022
8f5d02d
Add import comment test
TwitchBronBron Dec 21, 2022
101d8e3
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Feb 13, 2023
701a1aa
Merge branch 'release-0.66.0' of https://github.com/rokucommunity/bri…
TwitchBronBron Sep 26, 2023
ae67130
Fix broken tests. Add event hook warnings to pluginInterface
TwitchBronBron Oct 12, 2023
f6fb416
Merge branch 'release-0.66.0' of https://github.com/rokucommunity/bri…
TwitchBronBron Oct 12, 2023
51a4663
Merge branch 'release-0.66.0' into file-api
TwitchBronBron Oct 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions benchmarks/results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"lexer": {
"current": [
12645.727126865311,
13072.590947406612,
11663.650306847825
]
}
}
8 changes: 4 additions & 4 deletions benchmarks/targets/parse-brs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ module.exports = async (options: TargetOptions) => {
...options.additionalConfig
});
//collect all the brs file contents
const files = Object.values(builder.program.files).filter(x => ['.brs', '.bs', '.d.bs'].includes(x.extension)).map(x => ({
pkgPath: x.pkgPath,
fileContents: x.fileContents
const files = Object.values(builder.program.files).filter(x => ['.brs', '.bs', '.d.bs'].includes(brighterscript.util.getExtension(x.srcPath)!)).map(x => ({
destPath: x.destPath ?? x.pkgPath,
fileContents: (x as any).fileContents
}));
if (files.length === 0) {
console.log('[parse-brs] No brs files found in program');
Expand All @@ -31,7 +31,7 @@ module.exports = async (options: TargetOptions) => {
const promises: unknown[] = [];
for (const file of files) {
promises.push(
builder.program[setFileFuncName](file.pkgPath, file.fileContents)
builder.program[setFileFuncName](file.destPath, file.fileContents)
);
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Expand Down
10 changes: 7 additions & 3 deletions benchmarks/targets/parse-xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ module.exports = async (options: TargetOptions) => {
...options.additionalConfig
});
//collect all the XML file contents
const xmlFiles = Object.values(builder.program.files).filter(x => x.extension === '.xml').map(x => ({
const xmlFiles = Object.values(builder.program.files).filter(x => (x as any)?.extension === '.xml').map(x => ({
srcPath: x.srcPath ?? (x as any).pathAbsolute,
pkgPath: x.pkgPath,
fileContents: x.fileContents
fileContents: (x as any).fileContents
}));
if (xmlFiles.length === 0) {
console.log('[xml-parser] No XML files found in program');
Expand All @@ -28,7 +28,11 @@ module.exports = async (options: TargetOptions) => {
suite.add(fullName, (deferred) => {
const wait: Promise<any>[] = [];
for (const x of xmlFiles) {
const xmlFile = new XmlFile(x.srcPath, x.pkgPath, builder.program);
let xmlFile = new XmlFile({ srcPath: x.srcPath, destPath: (x as any)?.destPath ?? x.pkgPath, program: builder.program });
if (typeof xmlFile.srcPath !== 'string') {
//fallback to legacy constructor signature
xmlFile = (XmlFile as any)(x.srcPath, x.pkgPath, builder.program);
}
//handle async and sync parsing
const prom = xmlFile.parse(x.fileContents);
if (prom as any) {
Expand Down
21 changes: 21 additions & 0 deletions benchmarks/targets/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = async (suite, name, brighterscript, projectPath, options) => {
const { ProgramBuilder } = brighterscript;

let builder;
suite.add(name, (deferred) => {
builder = new ProgramBuilder();
builder.run({
cwd: projectPath,
createPackage: false,
copyToStaging: false,
//disable diagnostic reporting (they still get collected)
diagnosticFilters: ['**/*'],
logLevel: 'error'
}).finally(() => {
deferred.resolve();
});
}, {
...options,
'defer': true
});
};
214 changes: 189 additions & 25 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,15 @@ Full compiler lifecycle:
- `beforeProgramCreate`
- `afterProgramCreate`
- `afterScopeCreate` ("source" scope)
- For each file:
- `beforeFileParse`
- `afterFileParse`
- For each physical file:
- `beforeProvideFile`
- `onProvideFile`
- `afterProvideFile`
- `beforeFileParse` (deprecated)
- `afterFileParse` (deprecated)
- For each physical and virtual file
- `beforeAddFile`
- `afterAddFile`
- `afterScopeCreate` (component scope)
- `beforeProgramValidate`
- For each file:
Expand Down Expand Up @@ -150,17 +156,19 @@ export interface CompilerPlugin {
name: string;
//program events
beforeProgramCreate?: (builder: ProgramBuilder) => void;
afterProgramCreate?: (program: Program) => 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>;

beforeProgramTranspile?: (program: Program, entries: TranspileObj[], editor: Editor) => void;
afterProgramTranspile?: (program: Program, entries: TranspileObj[], editor: Editor) => void;

/**
* Emitted before the program starts collecting completions
Expand Down Expand Up @@ -188,17 +196,35 @@ export interface CompilerPlugin {
*/
afterProvideHover?: PluginHandler<AfterProvideHoverEvent>;

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;

onGetCodeActions?: PluginHandler<OnGetCodeActionsEvent>;
onGetSemanticTokens?: PluginHandler<OnGetSemanticTokensEvent>;

/**
* Called before a file is added to the program. This is triggered for every file (even virtual files emitted by other files)
*/
beforeProvideFile?: PluginHandler<BeforeProvideFileEvent>;
/**
* Give plugins the opportunity to handle parsing/validating a file
*/
provideFile?: PluginHandler<ProvideFileEvent>;
/**
* Called after a file was added to the program.
*/
afterProvideFile?: PluginHandler<AfterProvideFileEvent>;

beforeFileParse?: PluginHandler<BeforeFileParseEvent>;
afterFileParse?: (file: File) => void;

/**
* Called before each file is validated
*/
Expand All @@ -210,11 +236,13 @@ export interface CompilerPlugin {
/**
* Called after each file is validated
*/
afterFileValidate?: (file: BscFile) => void;
afterFileValidate?: (file: File) => void;

beforeFileTranspile?: PluginHandler<BeforeFileTranspileEvent>;
afterFileTranspile?: PluginHandler<AfterFileTranspileEvent>;
beforeFileDispose?: (file: BscFile) => void;
afterFileDispose?: (file: BscFile) => void;

beforeFileDispose?: (file: File) => void;
afterFileDispose?: (file: File) => void;
}

// related types:
Expand All @@ -223,17 +251,12 @@ interface FileObj {
dest: string;
}

interface SourceObj {
pathAbsolute: string;
source: string;
}

interface TranspileObj {
file: (BscFile);
file: File;
outputPath: string;
}

type ValidateHandler = (scope: Scope, files: BscFile[], callables: CallableContainerMap) => void;
type ValidateHandler = (scope: Scope, files: File[], callables: CallableContainerMap) => void;
interface CallableContainerMap {
[name: string]: CallableContainer[];
}
Expand Down Expand Up @@ -297,14 +320,14 @@ Note: in a language-server context, Scope validation happens every time a file c

```typescript
// bsc-plugin-no-underscores.ts
import { CompilerPlugin, BscFile, isBrsFile } from 'brighterscript';
import { CompilerPlugin, File, isBrsFile } from 'brighterscript';

// plugin factory
export default function () {
return {
name: 'no-underscores',
// post-parsing validation
afterFileValidate: (file: BscFile) => {
afterFileValidate: (file: File) => {
if (isBrsFile(file)) {
// visit function statements and validate their name
file.parser.references.functionStatements.forEach((fun) => {
Expand All @@ -326,7 +349,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 `AstEditor` 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 `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:
```brightscript
Expand Down Expand Up @@ -394,3 +417,144 @@ export default function plugin() {
} as CompilerPlugin;
}
```

## File API
By default, BrighterScript only parses files that it knows how to handle. Generally this includes `.xml` files in the compontents folder, `.brs`, `.bs` and `.d.bs` files. Other files may be handled in the future, such as `manifest`, `.ts` and possibly more. All other files are loaded into the program as `AssetFile` types and have no special handling or processing.

Plugins can provide files by contributing a `provideFile` function. BrighterScript will perform all of its file providing (like for `.xml` or `.brs` files) at the end of `provideFile` once every plugin had a chance to provide their own files. If you need to handle files before brighterscript does (like if you wanted to parse the .brs file instead of letting BrighterScript do it), you should do this in the `provideFile` event.

Your plugin may want to add enhanced features for file types (such as parsing javascript and converting it to BrightScript). BrighterScript supports this by asking plugins to "`provide`" file objects for a given file path.

Here's a sample plugin showing how to handle this:

```typescript
import { ProvideFileEvent, CompilerPlugin, BrsFile } from 'brighterscript';

export default function plugin() {
return {
name: 'removeCommentAndPrintStatements',
provideFile: (event: ProvideFileEvent) => {
//convert all javascript files into .brs files (magically!)
if (event.srcExtension === '.js') {
//get the file contents as a string
const jsCode = event.getFileData().toString();

//somehow magically convert javascript code to brightscript code
const brsCode = convertJsToBrsUsingMagic(jsCode);

//create a new BrsFile which will hold the final brs code after the js file was parsed
const file = event.fileFactory.BrsFile({
srcPath: event.srcPath,
//rename the .js extension to .brs
destPath: event.destPath.replace(/\.js$/, '.brs')
});
//parse the generated brs code
file.parse(brsCode);

//add this brs file to the event, which is how you "provide" the file
event.files.push(file);
}
}
} as CompilerPlugin;
}
```

### Multiple files
Plugins can also provide _multiple_ files from a single physical file. Consider this example:
```typescript


import { BeforeProvideFileEvent, CompilerPlugin, BrsFile, XmlFile, trim } from 'brighterscript';

export default function plugin() {
return {
name: 'componentPlugin',
beforeProvideFile: (event: BeforeProvideFileEvent) => {
// source/buttons.component.bs

event.files
//split a .component file into a .brs and a .xml file
if (event.srcExtension === '.component') {
//get the filename (we will use this as the component name)
const componentName = path.basename(event.srcPath);
//get the file contents as a string
const code = event.getFileData().toString();

//create a new BrsFile to act as the primary .brs script for this file
const brsFile = event.factory.BrsFile({
srcPath: event.srcPath.replace(/\.component$/, '.brs'),
destPath: event.destPath.replace(/\.component$/, '.brs')
});
//parse the generated brs code
brsFile.parse(code);
//add this brs file to the event, which is how you "provide" the file
event.files.push(brsFile);

//create an XmlFile which will serve as the SceneGraph component for this file
const xmlFile = event.fileFactory.XmlFile({
srcPath: event.srcPath.replace(/\.component$/, '.xml'),
destPath: event.destPath.replace(/\.component$/, '.xml')
});
xmlFile.parse(`
<component name="${componentName}">
<script uri="${event.destPath}" />
</component>
`);
//add this file to the event, which is how you "provide" the file
event.files.push(xmlFile);
}
}
} as CompilerPlugin;
}
```

### File Factory
Your plugin will be written against a specific version of BrighterScript. However, your plugin may be loaded by a different version of brighterscript (either by the brighterscript cli or through an editor like vscode). Running different versions of BrighterScript could cause issues in the file api.

To mitigate this, the `provideFile` events supply a `fileFactory`, which exposes the file classes from the runner's brighterscript version. When possible, use the file factories found in `event.fileFactory` instead of direct class constructors. (i.e. use `event.fileFactory.BrsFile` instead of `new BrsFile()`). By using the file factories, this ensures better interoperability between plugins and a wide range of brighterscript versions.

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){
if (isXmlFile(event.file)) {
// do XmlFile work
} else {
// assume it's a BrsFile (bad!)
// it could now be a .jpeg or .png
}
}
```

If a plugin has code like this, it may start failing due to receiving an `AssetFile` or some plugin-contributed custom file type that it didn't expect. We recommend that plugin authors always guard their code with file-specific conditional checks.

### srcPath, destPath, and pkgPath
The file api introduces a breaking change related to file paths. Previously there were only `srcPath` and `pkgPath`. `pkgPath` historically contained the file path as you would reference it in your project, such as `source/main.bs`. However, there was no property to represent its final path on device (i.e. `source/main.brs`).

To mitigate this, and since the file api is already causing a few breaking changes, we decided to change the way file paths work. `srcPath` remains the same. However, `pkgPath` has been renamed to `destPath` to represent the path to the file as it exists in your brighterscript project _before_ transpilation. `pkgPath` will now represent the final path where the file will reside on-device.

Plugin authors need to refactor their plugins to use `file.destPath` instead of `file.pkgPath`. While `destPath` and `pkgPath` sometimes have the same value, plugin authors should always assume that the paths are different.

Here's a description of each path property in BrighterScript now that the file api has been released.

- **srcPath** - the absolute path to the source file. For example:<br/>
`C:\projects\YourRokuApp\source\main.bs"` or `"/usr/projects/YourRokuApp/source/main.bs"`
- **destPath** - the path where the file exists within the context of a brightscript project, relative to the root of the package/zip.
This the path that brightscript engineers will use in their channel code. This should _not_ containing a leading slash or `pkg:/` scheme. For example:<br/>
`"source/main.bs"`
- **pkgPath** - the final path where the file will reside on-device. For example:<br/>
`"source/main.brs"`

Here's an example file showing all three paths:
```js
{
//location in source project
srcPath: "C:/projects/YourRokuApp/source/main.bs",
//location in brighterscript program
destPath: "source/main.bs"
//location on device
pkgPath: "source/main.brs"
}
```
2 changes: 1 addition & 1 deletion scripts/scrape-roku-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ class Runner {
}

private getMethod(text: string) {
// var state = new TranspileState(new BrsFile('', '', new Program({}));
// var state = new TranspileState(new BrsFile({ srcPath: '', destPath: '', program: new Program({})});
const functionSignatureToParse = `function ${this.fixFunctionParams(this.sanitizeMarkdownSymbol(text))}\nend function`;
const { statements } = Parser.parse(functionSignatureToParse);
if (statements.length > 0) {
Expand Down
Loading