diff --git a/src/BsConfig.ts b/src/BsConfig.ts
index dd9a1be4a..e35849b65 100644
--- a/src/BsConfig.ts
+++ b/src/BsConfig.ts
@@ -186,7 +186,15 @@ export interface BsConfig {
* @default true
*/
sourceMap?: boolean;
-
+ /**
+ * Excludes empty files from being included in the output. Some Brighterscript files
+ * are left empty or with only comments after transpilation to Brightscript.
+ * The default behavior is to write these to disk after transpilation.
+ * Setting this flag to `true` will prevent empty files being written and will
+ * remove associated script tags from XML
+ * @default false
+ */
+ pruneEmptyCodeFiles?: boolean;
/**
* Allow brighterscript features (classes, interfaces, etc...) to be included in BrightScript (`.brs`) files, and force those files to be transpiled.
* @default false
diff --git a/src/Program.spec.ts b/src/Program.spec.ts
index 5b2eb101e..b072cc928 100644
--- a/src/Program.spec.ts
+++ b/src/Program.spec.ts
@@ -2159,6 +2159,51 @@ describe('Program', () => {
s`${sourceRoot}/source/main.bs`
);
});
+
+ it('does not publish files that are empty', async () => {
+ let sourceRoot = s`${tempDir}/sourceRootFolder`;
+ program = new Program({
+ rootDir: rootDir,
+ stagingDir: stagingDir,
+ sourceRoot: sourceRoot,
+ sourceMap: true,
+ pruneEmptyCodeFiles: true
+ });
+ program.setFile('source/types.bs', `
+ enum mainstyle
+ dark = "dark"
+ light = "light"
+ end enum
+ `);
+ program.setFile('source/main.bs', `
+ import "pkg:/source/types.bs"
+
+ sub main()
+ ? "The night is " + mainstyle.dark + " and full of terror"
+ end sub
+ `);
+ await program.transpile([
+ {
+ src: s`${rootDir}/source/main.bs`,
+ dest: s`source/main.bs`
+ },
+ {
+ src: s`${rootDir}/source/types.bs`,
+ dest: s`source/types.bs`
+ }
+ ], stagingDir);
+
+ expect(trimMap(
+ fsExtra.readFileSync(s`${stagingDir}/source/main.brs`).toString()
+ )).to.eql(trim`
+ 'import "pkg:/source/types.bs"
+
+ sub main()
+ ? "The night is " + "dark" + " and full of terror"
+ end sub
+ `);
+ expect(fsExtra.pathExistsSync(s`${stagingDir}/source/types.brs`)).to.be.false;
+ });
});
describe('typedef', () => {
diff --git a/src/Program.ts b/src/Program.ts
index a6e36a3ae..5ffb9e9e1 100644
--- a/src/Program.ts
+++ b/src/Program.ts
@@ -1213,28 +1213,30 @@ export class Program {
//mark this file as processed so we don't process it more than once
processedFiles.add(outputPath?.toLowerCase());
- //skip transpiling typedef files
- if (isBrsFile(file) && file.isTypedef) {
- return;
- }
+ if (!this.options.pruneEmptyCodeFiles || !file.canBePruned) {
+ //skip transpiling typedef files
+ if (isBrsFile(file) && file.isTypedef) {
+ return;
+ }
- const fileTranspileResult = this._getTranspiledFileContents(file, outputPath);
+ const fileTranspileResult = this._getTranspiledFileContents(file, outputPath);
- //make sure the full dir path exists
- await fsExtra.ensureDir(path.dirname(outputPath));
+ //make sure the full dir path exists
+ await fsExtra.ensureDir(path.dirname(outputPath));
- if (await fsExtra.pathExists(outputPath)) {
- throw new Error(`Error while transpiling "${file.srcPath}". A file already exists at "${outputPath}" and will not be overwritten.`);
- }
- const writeMapPromise = fileTranspileResult.map ? fsExtra.writeFile(`${outputPath}.map`, fileTranspileResult.map.toString()) : null;
- await Promise.all([
- fsExtra.writeFile(outputPath, fileTranspileResult.code),
- writeMapPromise
- ]);
-
- if (fileTranspileResult.typedef) {
- const typedefPath = outputPath.replace(/\.brs$/i, '.d.bs');
- await fsExtra.writeFile(typedefPath, fileTranspileResult.typedef);
+ if (await fsExtra.pathExists(outputPath)) {
+ throw new Error(`Error while transpiling "${file.srcPath}". A file already exists at "${outputPath}" and will not be overwritten.`);
+ }
+ const writeMapPromise = fileTranspileResult.map ? fsExtra.writeFile(`${outputPath}.map`, fileTranspileResult.map.toString()) : null;
+ await Promise.all([
+ fsExtra.writeFile(outputPath, fileTranspileResult.code),
+ writeMapPromise
+ ]);
+
+ if (fileTranspileResult.typedef) {
+ const typedefPath = outputPath.replace(/\.brs$/i, '.d.bs');
+ await fsExtra.writeFile(typedefPath, fileTranspileResult.typedef);
+ }
}
};
diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts
index 6cc4ec7e8..cb0845dc0 100644
--- a/src/files/BrsFile.spec.ts
+++ b/src/files/BrsFile.spec.ts
@@ -182,6 +182,66 @@ describe('BrsFile', () => {
});
});
+ describe('canBePruned', () => {
+ it('returns false is target file has contains a function statement', () => {
+ program.setFile('source/main.brs', `
+ sub main()
+ print \`pkg:\`
+ end sub
+ `);
+ const file = program.getFile('source/main.brs');
+ expect(file.canBePruned).to.be.false;
+ });
+
+ it('returns false if target file contains a class statement', () => {
+ program.setFile('source/main.brs', `
+ class Animal
+ public name as string
+ end class
+ `);
+ const file = program.getFile('source/main.brs');
+ expect(file.canBePruned).to.be.false;
+ });
+
+ it('returns false if target file contains a class statement', () => {
+ program.setFile('source/main.brs', `
+ namespace Vertibrates.Birds
+ function GetDucks()
+ end function
+ end namespace
+ `);
+ const file = program.getFile('source/main.brs');
+ expect(file.canBePruned).to.be.false;
+ });
+
+ it('returns true if target file contains only enum', () => {
+ program.setFile('source/main.brs', `
+ enum Direction
+ up
+ down
+ left
+ right
+ end enum
+ `);
+ const file = program.getFile('source/main.brs');
+ expect(file.canBePruned).to.be.true;
+ });
+
+ it('returns true if target file is empty', () => {
+ program.setFile('source/main.brs', '');
+ const file = program.getFile('source/main.brs');
+ expect(file.canBePruned).to.be.true;
+ });
+
+ it('returns true if target file only has comments', () => {
+ program.setFile('source/main.brs', `
+ ' this is an interesting comment
+ `);
+ const file = program.getFile('source/main.brs');
+ expect(file.canBePruned).to.be.true;
+ });
+ });
+
describe('getScopesForFile', () => {
it('finds the scope for the file', () => {
let file = program.setFile('source/main.brs', ``);
diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts
index 9edef7fd8..19c74a567 100644
--- a/src/files/BrsFile.ts
+++ b/src/files/BrsFile.ts
@@ -77,6 +77,24 @@ export class BrsFile {
this.srcPath = value;
}
+ /**
+ * Will this file result in only comment or whitespace output? If so, it can be excluded from the output if that bsconfig setting is enabled.
+ */
+ public get canBePruned() {
+ let canPrune = true;
+ this.ast.walk(createVisitor({
+ FunctionStatement: () => {
+ canPrune = false;
+ },
+ ClassStatement: () => {
+ canPrune = false;
+ }
+ }), {
+ walkMode: WalkMode.visitStatements
+ });
+ return canPrune;
+ }
+
/**
* The parseMode used for the parser for this file
*/
diff --git a/src/files/XmlFile.spec.ts b/src/files/XmlFile.spec.ts
index 104c299ec..400b0ce30 100644
--- a/src/files/XmlFile.spec.ts
+++ b/src/files/XmlFile.spec.ts
@@ -899,6 +899,92 @@ describe('XmlFile', () => {
const code = file.transpile().code;
expect(code.endsWith(``)).to.be.true;
});
+
+ it('removes script imports if given file is not publishable', () => {
+ program.options.pruneEmptyCodeFiles = true;
+ program.setFile(`components/SimpleScene.bs`, `
+ enum simplescenetypes
+ hero
+ intro
+ end enum
+ `);
+
+ testTranspile(trim`
+
+
+
+
+ `, trim`
+
+
+
+
+ `, 'none', 'components/SimpleScene.xml');
+ });
+
+ it('removes extra imports found via dependencies if given file is not publishable', () => {
+ program.options.pruneEmptyCodeFiles = true;
+ program.setFile(`source/simplescenetypes.bs`, `
+ enum SimpleSceneTypes
+ world = "world"
+ end enum
+ `);
+ program.setFile(`components/SimpleScene.bs`, `
+ import "pkg:/source/simplescenetypes.bs"
+
+ sub init()
+ ? "Hello " + SimpleSceneTypes.world
+ end sub
+ `);
+
+ testTranspile(trim`
+
+
+
+
+ `, trim`
+
+
+
+
+
+ `, 'none', 'components/SimpleScene.xml');
+ });
+
+ it('removes imports of empty brightscript files', () => {
+ program.options.pruneEmptyCodeFiles = true;
+ program.setFile(`components/EmptyFile.brs`, '');
+ program.setFile(`components/SimpleScene.brs`, `
+ sub init()
+ ? "Hello World"
+ end sub
+ `);
+ testTranspile(trim`
+
+
+
+
+
+ `, trim`
+
+
+
+
+
+ `, 'none', 'components/SimpleScene.xml');
+ });
});
describe('Transform plugins', () => {
diff --git a/src/files/XmlFile.ts b/src/files/XmlFile.ts
index 23ce5a203..ae4380712 100644
--- a/src/files/XmlFile.ts
+++ b/src/files/XmlFile.ts
@@ -70,6 +70,11 @@ export class XmlFile {
public commentFlags = [] as CommentFlag[];
+ /**
+ * Will this file result in only comment or whitespace output? If so, it can be excluded from the output if that bsconfig setting is enabled.
+ */
+ readonly canBePruned = false;
+
/**
* The list of script imports delcared in the XML of this file.
* This excludes parent imports and auto codebehind imports
@@ -458,27 +463,47 @@ export class XmlFile {
this.program.logger.debug('XmlFile', chalk.green(this.pkgPath), ...args);
}
+ private checkScriptsForPublishableImports(scripts: SGScript[]): [boolean, SGScript[]] {
+ if (!this.program.options.pruneEmptyCodeFiles) {
+ return [false, scripts];
+ }
+ const publishableScripts = scripts.filter(script => {
+ const uriAttributeValue = script.attributes.find((v) => v.key.text === 'uri')?.value.text || '';
+ const pkgMapPath = util.getPkgPathFromTarget(this.pkgPath, uriAttributeValue);
+ let file = this.program.getFile(pkgMapPath);
+ if (!file && pkgMapPath.endsWith(this.program.bslibPkgPath)) {
+ return true;
+ }
+ if (!file && pkgMapPath.endsWith('.brs')) {
+ file = this.program.getFile(pkgMapPath.replace(/\.brs$/, '.bs'));
+ }
+ return !(file?.canBePruned);
+ });
+ return [publishableScripts.length !== scripts.length, publishableScripts];
+ }
+
/**
* Convert the brightscript/brighterscript source code into valid brightscript
*/
public transpile(): CodeWithSourceMap {
const state = new TranspileState(this.srcPath, this.program.options);
+ const originalScripts = this.ast.component?.scripts ?? [];
const extraImportScripts = this.getMissingImportsForTranspile().map(uri => {
const script = new SGScript();
script.uri = util.getRokuPkgPath(uri.replace(/\.bs$/, '.brs'));
return script;
});
- let transpileResult: SourceNode | undefined;
+ const [scriptsHaveChanged, publishableScripts] = this.checkScriptsForPublishableImports([
+ ...originalScripts,
+ ...extraImportScripts
+ ]);
- if (this.needsTranspiled || extraImportScripts.length > 0) {
+ let transpileResult: SourceNode | undefined;
+ if (this.needsTranspiled || extraImportScripts.length > 0 || scriptsHaveChanged) {
//temporarily add the missing imports as script tags
- const originalScripts = this.ast.component?.scripts ?? [];
- this.ast.component.scripts = [
- ...originalScripts,
- ...extraImportScripts
- ];
+ this.ast.component.scripts = publishableScripts;
transpileResult = new SourceNode(null, null, state.srcPath, this.parser.ast.transpile(state));
diff --git a/src/util.spec.ts b/src/util.spec.ts
index c82333c8b..d6a662e82 100644
--- a/src/util.spec.ts
+++ b/src/util.spec.ts
@@ -303,6 +303,12 @@ describe('util', () => {
expect(util.normalizeConfig({ emitDefinitions: 'true' }).emitDefinitions).to.be.false;
});
+ it('sets pruneEmptyCodeFiles to false by default, or true if explicitly true', () => {
+ expect(util.normalizeConfig({}).pruneEmptyCodeFiles).to.be.false;
+ expect(util.normalizeConfig({ pruneEmptyCodeFiles: true }).pruneEmptyCodeFiles).to.be.true;
+ expect(util.normalizeConfig({ pruneEmptyCodeFiles: false }).pruneEmptyCodeFiles).to.be.false;
+ });
+
it('loads project from disc', () => {
fsExtra.outputFileSync(s`${tempDir}/rootDir/bsconfig.json`, `{ "outFile": "customOutDir/pkg.zip" }`);
let config = util.normalizeAndResolveConfig({
diff --git a/src/util.ts b/src/util.ts
index 414d8adda..7d8a37eea 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -342,6 +342,7 @@ export class Util {
config.diagnosticSeverityOverrides = config.diagnosticSeverityOverrides ?? {};
config.diagnosticFilters = config.diagnosticFilters ?? [];
config.plugins = config.plugins ?? [];
+ config.pruneEmptyCodeFiles = config.pruneEmptyCodeFiles === true ? true : false;
config.autoImportComponentScript = config.autoImportComponentScript === true ? true : false;
config.showDiagnosticsInConsole = config.showDiagnosticsInConsole === false ? false : true;
config.sourceRoot = config.sourceRoot ? standardizePath(config.sourceRoot) : undefined;