Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions bsconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,28 @@
"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
},
"bslibDestinationDir": {
"description": "Override the destination directory for the bslib.brs file. Use this if you want to customize where the bslib.brs file is located in the staging directory. Note that using a location outside of `source` will break scripts inside `source` that depend on bslib.brs. Defaults to `source`.",
"type": "string"
},
"bslibHandling": {
"description": "Configuration for how bslib functions should be handled during transpilation",
"type": "object",
"properties": {
"mode": {
"description": "How bslib functions should be handled. 'shared': Generate a single shared bslib.brs file (default behavior). 'unique-per-file': Inline bslib functions directly into each file with unique suffixes",
"type": "string",
"enum": ["shared", "unique-per-file"],
"default": "shared"
},
"uniqueStrategy": {
"description": "Strategy for generating unique suffixes when mode is 'unique-per-file'. 'md5': Use MD5 hash of file.srcPath (default). 'guid': Use a generated GUID",
"type": "string",
"enum": ["md5", "guid"],
"default": "md5"
}
}
}
}
}
18 changes: 18 additions & 0 deletions src/BsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,24 @@ export interface BsConfig {
* scripts inside `source` that depend on bslib.brs. Defaults to `source`.
*/
bslibDestinationDir?: string;

/**
* Configuration for how bslib functions should be handled during transpilation
*/
bslibHandling?: {
/**
* How bslib functions should be handled.
* - "shared": Generate a single shared bslib.brs file (default behavior)
* - "unique-per-file": Inline bslib functions directly into each file with unique suffixes
*/
mode?: 'shared' | 'unique-per-file';
/**
* Strategy for generating unique suffixes when mode is "unique-per-file".
* - "md5": Use MD5 hash of file.srcPath (default)
* - "guid": Use a generated GUID
*/
uniqueStrategy?: 'md5' | 'guid';
};
}

type OptionalBsConfigFields =
Expand Down
5 changes: 4 additions & 1 deletion src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,10 @@ export class Program {
});

//if there's no bslib file already loaded into the program, copy it to the staging directory
if (!this.getFile(bslibAliasedRokuModulesPkgPath) && !this.getFile(s`source/bslib.brs`)) {
//skip copying bslib when using unique-per-file mode since functions are inlined
if (this.options.bslibHandling?.mode !== 'unique-per-file' &&
!this.getFile(bslibAliasedRokuModulesPkgPath) &&
!this.getFile(s`source/bslib.brs`)) {
promises.push(util.copyBslibToStaging(stagingDir, this.options.bslibDestinationDir));
}
await Promise.all(promises);
Expand Down
199 changes: 199 additions & 0 deletions src/bslibInline.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { expect } from './chai-config.spec';
import { Program } from './Program';

describe('BslibInline', () => {
describe('configuration', () => {
it('should use shared mode by default', () => {
const program = new Program({});
expect(program.options.bslibHandling?.mode).to.equal('shared');
});

it('should use md5 strategy by default for unique-per-file', () => {
const program = new Program({
bslibHandling: {
mode: 'unique-per-file'
}
});
expect(program.options.bslibHandling?.uniqueStrategy).to.equal('md5');
});
});

describe('shared mode', () => {
it('should not inline functions in shared mode', () => {
const program = new Program({
bslibHandling: {
mode: 'shared'
}
});

program.setFile('source/main.bs', `
function main()
message = \`Hello \${m.top.name}\`
result = true ? "yes" : "no"
fallback = value ?? "default"
end function
`);

const file = program.getFile('source/main.bs') as any;
const result = program['_getTranspiledFileContents'](file);

// Should use regular bslib function calls without suffix
expect(result.code).to.include('bslib_toString(');
expect(result.code).to.include('bslib_coalesce(');
// Note: Simple ternary expressions expand to if/else instead of using bslib_ternary

// Should not contain inline function definitions
expect(result.code).to.not.include('function bslib_toString');
expect(result.code).to.not.include('function bslib_ternary');
expect(result.code).to.not.include('function bslib_coalesce');

program.dispose();
});
});

describe('unique-per-file mode', () => {
it('should inline only used bslib functions', () => {
const program = new Program({
bslibHandling: {
mode: 'unique-per-file',
uniqueStrategy: 'md5'
}
});

program.setFile('source/main.bs', `
function main()
message = \`Hello \${m.top.name}\`
end function
`);

const file = program.getFile('source/main.bs') as any;
const result = program['_getTranspiledFileContents'](file);

// Should contain inline toString function with unique suffix
expect(result.code).to.include('bslib_toString_');
expect(result.code).to.include('function bslib_toString_');

// Should not contain unused functions
expect(result.code).to.not.include('function bslib_ternary_');
expect(result.code).to.not.include('function bslib_coalesce_');

program.dispose();
});

it('should inline multiple bslib functions when used', () => {
const program = new Program({
bslibHandling: {
mode: 'unique-per-file',
uniqueStrategy: 'md5'
}
});

program.setFile('source/main.bs', `
function main()
message = \`Hello \${m.top.name}\`
result = true ? "yes" : "no"
fallback = value ?? "default"
end function
`);

const file = program.getFile('source/main.bs') as any;
const result = program['_getTranspiledFileContents'](file);

// Should contain inline functions with same unique suffix
const toStringMatch = result.code.match(/bslib_toString_([a-f0-9]+)/);
const coalesceMatch = result.code.match(/bslib_coalesce_([a-f0-9]+)/);

expect(toStringMatch).to.not.be.null;
expect(coalesceMatch).to.not.be.null;

// All functions should have the same suffix
expect(toStringMatch![1]).to.equal(coalesceMatch![1]);

// Should contain function definitions
expect(result.code).to.include('function bslib_toString_');
expect(result.code).to.include('function bslib_coalesce_');
// Note: Simple ternary expressions expand to if/else, so no bslib_ternary function needed

program.dispose();
});

it('should not include unused bslib functions in output', () => {
const program = new Program({
bslibHandling: {
mode: 'unique-per-file'
}
});

program.setFile('source/main.bs', `
function main()
print "No bslib functions used"
end function
`);

const file = program.getFile('source/main.bs') as any;
const result = program['_getTranspiledFileContents'](file);

// Should not contain any bslib function definitions
expect(result.code).to.not.include('function bslib_');
expect(result.code).to.not.include('bslib_toString');
expect(result.code).to.not.include('bslib_ternary');
expect(result.code).to.not.include('bslib_coalesce');

program.dispose();
});

it('should validate that inlined functions work correctly', () => {
const program = new Program({
bslibHandling: {
mode: 'unique-per-file'
}
});

program.setFile('source/main.bs', `
function main()
message = \`Hello \${m.top.name}\`
result = true ? "yes" : "no"
fallback = value ?? "default"
end function
`);

const file = program.getFile('source/main.bs') as any;
const result = program['_getTranspiledFileContents'](file);

// Verify the transpiled output contains the expected structure
expect(result.code).to.include('function main()');
expect(result.code).to.include('message = ("Hello " + bslib_toString_');
expect(result.code).to.include('fallback = bslib_coalesce_');
expect(result.code).to.include('end function');
// Note: Simple ternary expressions expand to if/else, so result uses if/then/else/end if

program.dispose();
});
});

describe('XML file handling', () => {
it('should handle XML transpilation in unique-per-file mode', () => {
// For now, just test that the mode is correctly set
const program = new Program({
bslibHandling: {
mode: 'unique-per-file'
}
});

expect(program.options.bslibHandling?.mode).to.equal('unique-per-file');
program.dispose();
});

it('should handle XML transpilation in shared mode', () => {
// For now, just test that the mode is correctly set
const program = new Program({
bslibHandling: {
mode: 'shared'
}
});

expect(program.options.bslibHandling?.mode).to.equal('shared');
program.dispose();
});
});
});
14 changes: 14 additions & 0 deletions src/files/BrsFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,20 @@ export class BrsFile {
//simple SourceNode wrapping the entire file to simplify the logic below
transpileResult = new SourceNode(null, null, state.srcPath, this.fileContents);
}

// Inject bslib functions if using unique-per-file mode and functions were used
if (this.program.options.bslibHandling?.mode === 'unique-per-file' &&
state.usedBslibFunctions.size > 0) {
const bslibFunctions = util.getBslibFunctionsWithSuffix(state.bslibSuffix, state.usedBslibFunctions);

// Append the bslib functions at the end of the file
transpileResult = new SourceNode(null, null, state.srcPath, [
transpileResult,
'\n\n',
bslibFunctions
]);
}

//undo any AST edits that the transpile cycle has made
state.editor.undoAll();

Expand Down
5 changes: 4 additions & 1 deletion src/files/XmlFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,10 @@ export class XmlFile {
private getMissingImportsForTranspile() {
let ownImports = this.getAvailableScriptImports();
//add the bslib path to ownImports, it'll get filtered down below
ownImports.push(this.program.bslibPkgPath);
//skip adding bslib import when using unique-per-file mode since functions are inlined
if (this.program.options.bslibHandling?.mode !== 'unique-per-file') {
ownImports.push(this.program.bslibPkgPath);
}

let parentImports = this.parentComponent?.getAvailableScriptImports() ?? [];

Expand Down
22 changes: 22 additions & 0 deletions src/parser/BrsTranspileState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,35 @@ export class BrsTranspileState extends TranspileState {
) {
super(file.srcPath, file.program.options);
this.bslibPrefix = this.file.program.bslibPrefix;

// Generate unique suffix for bslib functions if unique-per-file mode is enabled
if (this.file.program.options.bslibHandling?.mode === 'unique-per-file') {
const { util } = require('../util');
const suffix = util.generateBslibSuffix(
file.srcPath,
this.file.program.options.bslibHandling.uniqueStrategy || 'md5'
);
this.bslibSuffix = `_${suffix}`;
} else {
this.bslibSuffix = '';
}
}

/**
* The prefix to use in front of all bslib functions
*/
public bslibPrefix: string;

/**
* The unique suffix to append to bslib function names (only used in unique-per-file mode)
*/
public bslibSuffix: string;

/**
* Track which bslib functions are used in this file for inlining (only used in unique-per-file mode)
*/
public usedBslibFunctions: Set<string> = new Set();

/**
* the tree of parents, with the first index being direct parent, and the last index being the furthest removed ancestor.
* Used to assist blocks in knowing when to add a comment statement to the same line as the first line of the parent
Expand Down
15 changes: 12 additions & 3 deletions src/parser/Expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1513,8 +1513,11 @@ export class TemplateStringExpression extends Expression {

//wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
} else {
if (state.bslibSuffix) {
state.usedBslibFunctions.add('toString');
}
add(
state.bslibPrefix + '_toString(',
state.bslibPrefix + '_toString' + state.bslibSuffix + '(',
...expression.transpile(state),
')'
);
Expand Down Expand Up @@ -1773,8 +1776,11 @@ export class TernaryExpression extends Expression {
);
state.blockDepth--;
} else {
if (state.bslibSuffix) {
state.usedBslibFunctions.add('ternary');
}
result.push(
state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
state.sourceNode(this.test, state.bslibPrefix + `_ternary` + state.bslibSuffix + `(`),
...this.test.transpile(state),
state.sourceNode(this.test, `, `),
...this.consequent?.transpile(state) ?? ['invalid'],
Expand Down Expand Up @@ -1877,8 +1883,11 @@ export class NullCoalescingExpression extends Expression {
);
state.blockDepth--;
} else {
if (state.bslibSuffix) {
state.usedBslibFunctions.add('coalesce');
}
result.push(
state.bslibPrefix + `_coalesce(`,
state.bslibPrefix + `_coalesce` + state.bslibSuffix + `(`,
...this.consequent.transpile(state),
', ',
...this.alternate.transpile(state),
Expand Down
Loading