Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f5b045a
Base tree shaking implementation
iObject Mar 18, 2026
022dd84
bsconfig control
iObject Mar 18, 2026
f04302e
Change from annotation to comments
iObject Mar 18, 2026
0ab9294
spec update
iObject Mar 18, 2026
adf11e7
Potential fix for pull request finding
iObject Mar 18, 2026
b6aed2e
Potential fix for pull request finding
iObject Mar 18, 2026
5d18b91
correct tests
iObject Mar 18, 2026
208d9d6
remove only
iObject Mar 18, 2026
e690a39
bsconfig.schema.json update
iObject Mar 18, 2026
0d70977
readme updates
iObject Mar 18, 2026
b001258
spec rename
iObject Mar 18, 2026
e226ecd
documentation fix
iObject Mar 18, 2026
c58c812
runscreensaver
iObject Mar 18, 2026
7ee5624
optimizations
iObject Mar 18, 2026
19f13f8
Potential fix for pull request finding
iObject Mar 18, 2026
6b6a080
Merge branch 'master' into 183-tree-shaking
iObject Mar 18, 2026
def4082
Potential fix for pull request finding
iObject Mar 18, 2026
e5d83aa
resolve issues
iObject Mar 18, 2026
421d77b
fix shorthand conflict
iObject Mar 18, 2026
1f32d43
Potential fix for pull request finding
iObject Mar 18, 2026
4879eb7
Potential fix for pull request finding
iObject Mar 18, 2026
18f7fd5
Potential fix for pull request finding
iObject Mar 18, 2026
cf64d4c
fix bug
iObject Mar 19, 2026
de7dece
Windows src path normalization
iObject Mar 19, 2026
0569d01
Tree shaking now lives in Program.beforeProgramTranspile
iObject Mar 19, 2026
5ca9b74
remove space
iObject Mar 19, 2026
4296778
Potential fix for pull request finding
iObject Mar 19, 2026
ff2d8df
Move TreeShaker
iObject Mar 19, 2026
949f67b
Merge branch '183-tree-shaking' of github.com:rokucommunity/brighters…
iObject Mar 19, 2026
edd9cea
Updated comment
iObject Mar 19, 2026
301a919
Potential fix for pull request finding
iObject Mar 19, 2026
12643fd
logging
iObject Mar 19, 2026
b6d9d57
fix(tree-shaker): retain .brs library functions called via namespace …
iObject Mar 19, 2026
bbc8752
move logging to single output
iObject Mar 19, 2026
83e0f6b
tests for excludes brs
iObject Mar 19, 2026
b6f036a
Move treeShaker back into BscPlugin.ts
iObject Mar 19, 2026
7672891
remove whitespace
iObject Mar 19, 2026
5986f59
readme update
iObject Mar 19, 2026
c4d231d
omit fully protected files from transpiler
iObject Mar 19, 2026
a9b0de5
Fix issue
iObject Mar 19, 2026
e3717f1
pattern fix
iObject Mar 19, 2026
94f9c9d
treeShaking normalization tests
iObject Mar 19, 2026
8d6d302
changes map for any compiled rule that ends up with no criteria
iObject Mar 19, 2026
9267d14
Merge branch 'master' into 183-tree-shaking
iObject Mar 19, 2026
84a4da2
readme update
iObject Mar 19, 2026
ac3d832
customizations support
iObject Mar 20, 2026
45a03cf
Merge branch '183-tree-shaking' of github.com:rokucommunity/brighters…
iObject Mar 20, 2026
41bcae3
test for function passed by reference as an argument to another function
iObject Mar 20, 2026
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
60 changes: 60 additions & 0 deletions bsconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,66 @@
"description": "Allow brighterscript features (classes, interfaces, etc...) to be included in BrightScript (`.brs`) files, and force those files to be transpiled.",
"type": "boolean",
"default": false
},
"treeShaking": {
"description": "Configuration for tree shaking (dead code elimination). Disabled by default; set `enabled: true` to opt in.",
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"description": "Enable tree shaking. Defaults to false.",
"type": "boolean",
"default": false
},
"keep": {
"description": "List of keep rules. Functions matching any rule are always retained along with their full dependency closure. A plain string is shorthand for `{ functions: [string] }`.",
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"description": "Exact BrightScript function name to always keep."
},
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"description": "Keep rule object. All specified fields must match (AND semantics). Rules are combined with OR semantics.",
"properties": {
"src": {
"description": "Glob pattern(s) matched against the source file path.",
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" } }
]
},
"dest": {
"description": "Glob pattern(s) matched against the package-relative destination path.",
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" } }
]
},
"functions": {
"description": "Exact BrightScript function name(s) to keep.",
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" } }
]
},
"matches": {
"description": "Glob/wildcard pattern(s) matched against the BrightScript function name.",
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" } }
]
}
}
}
]
}
}
}
}
}
}
15 changes: 15 additions & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,18 @@ second line text`
```brighterscript
authStatus = user <> invalid ? "logged in" : "not logged in"
```

## [Tree Shaking](shaking.md)
Tree shaking removes unused functions from your transpiled output. Opt in via `bsconfig.json`, then use `' bs:keep` to protect functions the static analysis can't see.
```json
{
"treeShaking": {
"enabled": true
}
}
```
```brightscript
sub onDynamicCallback() ' bs:keep
' won't be removed even with no visible callers
end sub
```
224 changes: 224 additions & 0 deletions docs/shaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Tree Shaking

Tree shaking is BrighterScript's dead code elimination feature. When enabled, any function that is never called — directly or indirectly — is removed from the transpiled output, reducing the size of your deployed channel.

Tree shaking is **disabled by default**. You must explicitly opt in.

## Enabling Tree Shaking

Add a `treeShaking` section to your `bsconfig.json`:

```json
{
"treeShaking": {
"enabled": true
}
}
```

That's the minimal configuration. With only `enabled: true`, any function that cannot be reached from a known entry point will be removed.

## How It Works

BrighterScript performs a two-pass analysis across the entire program before transpiling:

**Pass 1 — collect definitions.** Every `sub` and `function` statement in every `.bs`/`.brs` file is recorded, along with its source file and its transpiled (BrightScript) name. `bs:keep` comments are also detected in this pass (see below). Functions declared in XML `<interface>` elements and `onChange` callbacks are collected from `.xml` component files.

**Pass 2 — collect references.** The AST of every file is walked to find:
- Direct call expressions (`doSomething()`, `myNamespace.helper()`)
- String literals that look like identifiers — conservatively retained to support dynamic dispatch patterns like `observeField("field", "onMyFieldChanged")` and `callFunc`
- Variable expressions that reference a known function name (function-by-reference patterns such as `m.observe(node, "field", onContentChanged)`)
- `@.` callFunc shorthand expressions

After both passes, any function that has no references and is not a protected entry point is removed from the transpiled output by replacing its statement with an empty node.

### Protected Entry Points

The following Roku framework callbacks are **always kept** regardless of whether they appear in any call expression:

| Name | Context |
|---|---|
| `main` | Channel entry point |
| `init` | SceneGraph component lifecycle |
| `onKeyEvent` | Remote key handling |
| `onMessage` | Task/port message handling |
| `runUserInterface` | UI task entry point |
| `runTask` | Background task entry point |

## `bs:keep` Comments

A `bs:keep` comment tells the tree shaker to unconditionally keep a specific function, even if it has no detectable callers. This is useful for functions that are invoked dynamically at runtime in ways the static analysis cannot see.

### Same-Line

Place the comment on the same line as the `sub` or `function` keyword:

```brightscript
sub onMyDynamicCallback() ' bs:keep
' ...
end sub
```

### Above the Function

Place the comment anywhere between the end of the previous function and the start of the next one:

```brightscript
end sub

' bs:keep
sub onMyDynamicCallback()
' ...
end sub
```

Multiple lines of other comments or blank lines between `bs:keep` and the function are fine — the comment applies to the next function that follows it.

### First Function in a File

For the very first function in a file, `bs:keep` can appear anywhere before it (since there is no previous function to bound the region):

```brightscript
' This file's public API — prevent tree shaking
' bs:keep
sub publicEntry()
' ...
end sub
```

### `rem` Syntax

Both `'` and `rem` comment starters are supported:

```brightscript
rem bs:keep
sub legacyEntryPoint()
' ...
end sub
```

### What `bs:keep` Does NOT Do

- A `bs:keep` comment placed **inside** a function body does not protect that function.
- A `bs:keep` comment does not automatically keep the functions that the annotated function calls — only the annotated function itself. If those callees are also unused by the rest of the program, they will still be removed. Use [`treeShaking.keep`](#treeshakingkeep-rules) rules with dependency closure, or annotate each callee individually, if you need to retain an entire call graph.

## `treeShaking.keep` Rules

For coarser-grained control — keeping entire files, namespaces, or pattern-matched sets of functions — use the `keep` array in `bsconfig.json`. Each entry is either a plain string (exact function name) or a rule object.

### Plain String

A plain string matches the exact transpiled (BrightScript) function name, case-insensitively:

```json
{
"treeShaking": {
"enabled": true,
"keep": [
"myPublicFunction",
"myNamespace_helperFunction"
]
}
}
```

For namespaced BrighterScript functions, use the transpiled underscore form. For example, `namespace myNamespace` + `function helperFunction()` transpiles to `myNamespace_helperFunction`.

### Rule Objects

A rule object can filter by any combination of `functions`, `matches`, `src`, and `dest`. All fields present in a single rule must match simultaneously (AND semantics). Rules in the array are evaluated independently and a function is kept if **any** rule matches (OR semantics).

#### `functions` — exact name list

```json
{
"keep": [
{ "functions": "myNamespace_init" },
{ "functions": ["analyticsTrack", "analyticsFlush"] }
]
}
```

#### `matches` — glob/wildcard against the function name

```json
{
"keep": [
{ "matches": "analytics_*" },
{ "matches": ["debug_*", "test_*"] }
]
}
```

#### `src` — glob against the source file path

The pattern is resolved relative to `rootDir` unless it is an absolute path.

```json
{
"keep": [
{ "src": "source/public/**/*.bs" },
{ "src": ["source/api.bs", "source/auth.bs"] }
]
}
```

#### `dest` — glob against the package-relative destination path

This matches the path the file will have inside the deployed zip (e.g. `pkg:/source/utils.brs`).

```json
{
"keep": [
{ "dest": "source/public/**/*.brs" }
]
}
```

#### Combining Fields (AND within a rule)

Keep only functions whose name starts with `api_` **and** that live in a specific file:

```json
{
"keep": [
{
"src": "source/api.bs",
"matches": "api_*"
}
]
}
```

### Dependency Closure

Keep rules do not automatically pull in the transitive dependencies of a matched function. If `api_login` calls `crypto_hash` and only `api_login` is matched by a keep rule, `crypto_hash` will still be removed if nothing else calls it.

To retain the entire reachable graph of a kept function, either:
- Add a `bs:keep` comment to each function you want to preserve, or
- Add additional keep rules (e.g. a `src` rule that covers the whole file containing the helpers)

## Configuration Reference

```json
{
"treeShaking": {
"enabled": false,
"keep": []
}
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | `boolean` | `false` | Must be `true` to activate tree shaking |
| `keep` | `(string \| KeepRule)[]` | `[]` | Functions matching any entry are always retained |

**KeepRule fields** (all optional; at least one required):

| Field | Type | Description |
|---|---|---|
| `functions` | `string \| string[]` | Exact transpiled function name(s), case-insensitive |
| `matches` | `string \| string[]` | Glob pattern(s) matched against the transpiled function name |
| `src` | `string \| string[]` | Glob pattern(s) matched against the source file path |
| `dest` | `string \| string[]` | Glob pattern(s) matched against the package-relative destination path |
56 changes: 54 additions & 2 deletions src/BsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,57 @@ export interface BsConfig {
* scripts inside `source` that depend on bslib.brs. Defaults to `source`.
*/
bslibDestinationDir?: string;

/**
* Configuration for tree shaking (dead code elimination).
*/
treeShaking?: TreeShakingConfig;
}

/**
* A single keep-rule entry in `treeShaking.keep`.
*
* A plain string is shorthand for `{ functions: [string] }`.
* An object entry must contain at least one of `src`, `dest`, `functions`, or `matches`.
* All fields present on one rule are combined with AND semantics.
* Rules across the list are combined with OR semantics.
*/
export type TreeShakingKeepEntry = string | TreeShakingKeepRule;

export interface TreeShakingKeepRule {
/** Glob pattern(s) matched against the declaration's source file path. */
src?: string | string[];
/** Glob pattern(s) matched against the declaration's package-relative destination path. */
dest?: string | string[];
/** Exact function name(s) to keep (BrightScript/transpiled names). */
functions?: string | string[];
/** Glob/wildcard pattern(s) matched against the function name (BrightScript/transpiled names). */
matches?: string | string[];
}

export interface TreeShakingConfig {
/**
* Enable or disable tree shaking. Defaults to `false` (opt-in).
*/
enabled?: boolean;
/**
* Declarations matching any rule in this list are always retained,
* along with their full dependency closure.
*/
keep?: TreeShakingKeepEntry[];
}

/** Normalized internal form produced by `normalizeConfig`. */
export interface NormalizedKeepRule {
src?: string[];
dest?: string[];
functions?: string[];
matches?: string[];
}

export interface NormalizedTreeShakingConfig {
enabled: boolean;
keep: NormalizedKeepRule[];
}

type OptionalBsConfigFields =
Expand All @@ -231,5 +282,6 @@ type OptionalBsConfigFields =
| 'stagingDir';

export type FinalizedBsConfig =
Omit<Required<BsConfig>, OptionalBsConfigFields>
& Pick<BsConfig, OptionalBsConfigFields>;
Omit<Required<BsConfig>, OptionalBsConfigFields | 'treeShaking'>
& Pick<BsConfig, OptionalBsConfigFields>
& { treeShaking: NormalizedTreeShakingConfig };
Loading
Loading