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

Prevent publishing of empty files #997

10 changes: 9 additions & 1 deletion src/BsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions src/Program.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
40 changes: 21 additions & 19 deletions src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
};

Expand Down
60 changes: 60 additions & 0 deletions src/files/BrsFile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', ``);
Expand Down
18 changes: 18 additions & 0 deletions src/files/BrsFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
86 changes: 86 additions & 0 deletions src/files/XmlFile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,92 @@ describe('XmlFile', () => {
const code = file.transpile().code;
expect(code.endsWith(`<!--//# sourceMappingURL=./SimpleScene.xml.map -->`)).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`
<?xml version="1.0" encoding="utf-8" ?>
<component
name="SimpleScene" extends="Scene"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd"
>
<script type="text/brightscript" uri="SimpleScene.bs"/>
</component>
`, trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="SimpleScene" extends="Scene" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd">
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
</component>
`, '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`
<?xml version="1.0" encoding="utf-8" ?>
<component
name="SimpleScene" extends="Scene"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd"
>
<script type="text/brightscript" uri="SimpleScene.bs"/>
</component>
`, trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="SimpleScene" extends="Scene" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd">
<script type="text/brightscript" uri="SimpleScene.brs" />
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
</component>
`, '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`
<?xml version="1.0" encoding="utf-8" ?>
<component
name="SimpleScene" extends="Scene"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd"
>
<script type="text/brightscript" uri="SimpleScene.brs"/>
<script type="text/brightscript" uri="EmptyFile.brs"/>
</component>
`, trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="SimpleScene" extends="Scene" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd">
<script type="text/brightscript" uri="SimpleScene.brs" />
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
</component>
`, 'none', 'components/SimpleScene.xml');
});
});

describe('Transform plugins', () => {
Expand Down
39 changes: 32 additions & 7 deletions src/files/XmlFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));

Expand Down
6 changes: 6 additions & 0 deletions src/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ describe('util', () => {
expect(util.normalizeConfig(<any>{ 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({
Expand Down
1 change: 1 addition & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading