New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

bsc-plugin-auto-findnode

Package Overview
Dependencies
Maintainers
0
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bsc-plugin-auto-findnode - npm Package Compare versions

Comparing version 0.1.0 to 0.1.1

bsc-plugin-auto-findnode-0.1.1.tgz

3

.eslintrc.js

@@ -200,3 +200,4 @@ module.exports = {

rules: {
'@typescript-eslint/no-unsafe-assignment': 'off'
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unused-expressions': 'off'
}

@@ -203,0 +204,0 @@ }

@@ -10,2 +10,10 @@ # Changelog

## [0.1.1](https://github.com/rokucommunity/bsc-plugin-auto-findnode/compare/v0.1.0...v0.1.1) - 2025-01-09
### Fixed
- Fix missing xml import for newly-generated codebehind file ([#8](https://github.com/rokucommunity/bsc-plugin-auto-findnode/pull/8))
- Better handling of the codebehind file ([#0](https://github.com/rokucommunity/bsc-plugin-auto-findnode/pull/9))
- Fix some formatting for the `init()` function in new files ([#10](https://github.com/rokucommunity/bsc-plugin-auto-findnode/pull/10))
## [0.1.0](https://github.com/rokucommunity/bsc-plugin-auto-findnode/compare/315acc957e1f9e26fa8398d1f6f1926c592355a8...v0.1.0) - 2024-08-19

@@ -12,0 +20,0 @@ - Initial release, which includes:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateNodeWithIDInjection = exports.findNodeWithIDInjection = void 0;
exports.validateNodeWithIDInjection = exports.findNodeWithIDInjection = exports.findChildrenWithIDs = void 0;
const brighterscript_1 = require("brighterscript");
const SGTypes_1 = require("brighterscript/dist/parser/SGTypes");
function findChildrenWithIDs(children) {
var _a, _b, _c, _d, _e;
let foundIDs = new Map();
if (children) {
children.forEach(child => {
var _a, _b, _c;
if (child.id) {
foundIDs.set(child.id, (_c = (_b = (_a = child.attributes.find(x => x.key.text === 'id')) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.range) !== null && _c !== void 0 ? _c : brighterscript_1.util.createRange(0, 0, 0, 100));
}
const subChildren = findChildrenWithIDs(child.children);
foundIDs = new Map([...foundIDs, ...subChildren]);
});
for (const child of children !== null && children !== void 0 ? children : []) {
if (child.id) {
foundIDs.set(child.id, (_e = (_d = (_c = (_b = (_a = child.attributes) === null || _a === void 0 ? void 0 : _a.find) === null || _b === void 0 ? void 0 : _b.call(_a, x => x.key.text === 'id')) === null || _c === void 0 ? void 0 : _c.value) === null || _d === void 0 ? void 0 : _d.range) !== null && _e !== void 0 ? _e : brighterscript_1.util.createRange(0, 0, 0, 100));
}
const subChildren = findChildrenWithIDs(child.children);
foundIDs = new Map([...foundIDs, ...subChildren]);
}
return foundIDs;
}
exports.findChildrenWithIDs = findChildrenWithIDs;
function findNodeWithIDInjection(program, entries, editor, createdFiles) {

@@ -25,40 +25,31 @@ var _a, _b, _c;

const ids = findChildrenWithIDs((_c = (_b = (_a = xmlFile.parser.ast.component) === null || _a === void 0 ? void 0 : _a.children) === null || _b === void 0 ? void 0 : _b.children) !== null && _c !== void 0 ? _c : []);
if (ids.size > 0) {
const scopeFiles = scope.getOwnFiles();
//find an init function from all the scope's files
let initFunction;
let hasBrsFile = false;
for (const file of scopeFiles) {
if ((0, brighterscript_1.isBrsFile)(file)) {
hasBrsFile = true;
initFunction = file.parser.references.functionStatementLookup.get('init');
if (initFunction) {
break;
}
}
}
if (!hasBrsFile) {
createdFiles.push(program.setFile(xmlFile.pkgPath.replace('.xml', '.bs'), ''));
}
//create an init function if it's missing
if (!initFunction) {
const codeBehindFile = program.getFiles(xmlFile.possibleCodebehindPkgPaths).find(x => !!x);
initFunction = brighterscript_1.Parser.parse(`sub init()\nend sub`).statements[0];
if (codeBehindFile) {
editor.arrayPush(codeBehindFile.parser.statements, initFunction);
}
}
if (initFunction) {
//add m variables for every xml component that has an id
// eslint-disable-next-line max-statements-per-line, @typescript-eslint/brace-style
const assignments = Array.from(ids).map(([id, range]) => { return `m.${id} = m.top.findNode("${id}")`; }).join('\n');
const statements = brighterscript_1.Parser.parse(`
sub temp()
${assignments}
end sub
`).statements[0].func.body.statements;
//add the assignments to the top of the init function
editor.arrayUnshift(initFunction.func.body.statements, ...statements);
}
//skip this xml file if there are no nodes with IDs in it
if (ids.size === 0) {
continue;
}
//build the list of assignments
const assignments = Array.from(ids).map(([id, range]) => {
return ` m.${id} = m.top.findNode("${id}")`;
}).join('\n');
const initFunctionText = `sub init()\n${assignments}\nend sub`;
const initFunctionInfo = findInitFunction(scope);
//if we found an init function, inject the assignments
if (initFunctionInfo) {
//add the assignments to the top of the init function
editor.arrayUnshift(initFunctionInfo.initFunction.func.body.statements, ...brighterscript_1.Parser.parse(initFunctionText).ast.statements[0].func.body.statements);
//we don't have an init function, create a new file and insert an empty init function into it
}
else {
//get a unique filename for the new file
const pkgPath = getUniqueFilename(xmlFile, program);
//create and add the new file to the program
const brsFileWithInit = program.setFile(pkgPath, initFunctionText);
createdFiles.push(brsFileWithInit);
//import this file into the current xml file
editor.arrayPush(xmlFile.parser.ast.component.scripts, new SGTypes_1.SGScript({
text: 'script'
}, [
(0, brighterscript_1.createSGAttribute)('uri', brighterscript_1.util.sanitizePkgPath(brsFileWithInit.pkgPath))
]));
}
}

@@ -69,3 +60,3 @@ }

function validateNodeWithIDInjection(program) {
var _a, _b, _c;
var _a, _b, _c, _d;
for (const scope of program.getScopes()) {

@@ -76,14 +67,3 @@ if ((0, brighterscript_1.isXmlScope)(scope)) {

if (ids.size > 0) {
const scopeFiles = scope.getOwnFiles();
let initFunction;
let initFunctionFile;
for (const file of scopeFiles) {
if ((0, brighterscript_1.isBrsFile)(file)) {
initFunction = file.parser.references.functionStatementLookup.get('init');
if (initFunction) {
initFunctionFile = file;
break;
}
}
}
const { initFunction, file: initFunctionFile } = (_d = findInitFunction(scope)) !== null && _d !== void 0 ? _d : {};
if (initFunction && initFunctionFile) {

@@ -122,2 +102,33 @@ initFunction.func.body.walk((0, brighterscript_1.createVisitor)({

exports.validateNodeWithIDInjection = validateNodeWithIDInjection;
/**
* Find the first function called `init()` across all files in a scope
*/
function findInitFunction(scope) {
for (const file of scope.getOwnFiles()) {
if ((0, brighterscript_1.isBrsFile)(file)) {
const initFunction = file.parser.references.functionStatementLookup.get('init');
if (initFunction) {
return {
initFunction: initFunction,
file: file
};
}
}
}
}
/**
* Get a pkgPath for a new brs file that will sit next to the given xml file. This is deterministic,
* so if the file already exists, we'll append the next available number number to the end of the filename to make it unique.
* @param file the xml file that we want to make a new brs file for
* @param program the bsc program (used for file name collision detection)
*/
function getUniqueFilename(file, program) {
let pkgPath = file.pkgPath.replace('.xml', '-findnode');
let sequence = 2;
//try up to 10 times to find a unique filename within the program
while (sequence < 10 && program.hasFile(`${pkgPath}.brs`) || program.hasFile(`${pkgPath}.bs`)) {
pkgPath = file.pkgPath.replace('.xml', `-findnode-${sequence++}`);
}
return `${pkgPath}.brs`;
}
//# sourceMappingURL=findNodes.js.map

@@ -43,10 +43,73 @@ "use strict";

const undent_1 = __importDefault(require("undent"));
const fsExtra = __importStar(require("fs-extra"));
const findNodes_1 = require("./findNodes");
const tempDir = (0, brighterscript_1.standardizePath) `${__dirname}/../.tmp`;
const rootDir = (0, brighterscript_1.standardizePath) `${tempDir}/rootDir`;
const stagingDir = (0, brighterscript_1.standardizePath) `${tempDir}/stagingDir`;
describe('findnode', () => {
let program;
const rootDir = path.join(__dirname, '../.tmp');
beforeEach(() => {
program = new brighterscript_1.Program({ rootDir: rootDir });
fsExtra.emptyDirSync(tempDir);
fsExtra.emptyDirSync(rootDir);
fsExtra.emptyDirSync(stagingDir);
program = new brighterscript_1.Program({
rootDir: rootDir,
stagingDir: stagingDir
});
program.plugins.add(new Plugin_1.Plugin());
});
it('it works when a bs file is present', () => __awaiter(void 0, void 0, void 0, function* () {
afterEach(() => {
fsExtra.removeSync(tempDir);
});
describe('findChildrenWithIDs', () => {
it('does not crash on undefined children array', () => {
(0, chai_1.expect)(Object.entries((0, findNodes_1.findChildrenWithIDs)(undefined))).to.eql([]);
});
it('does not crash when child is missing id prop', () => {
(0, chai_1.expect)(Object.entries((0, findNodes_1.findChildrenWithIDs)([{}]))).to.eql([]);
});
it('does not crash when child is missing id prop', () => {
(0, chai_1.expect)(Object.entries((0, findNodes_1.findChildrenWithIDs)([{
id: 1
}]))).to.eql([]);
});
});
describe('findNodeWithIDInjection', () => {
it('does not crash when is missing children', () => {
const file = program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
</component>
`);
delete file.parser.ast.component;
(0, findNodes_1.findNodeWithIDInjection)(program, [], new brighterscript_1.AstEditor(), []);
});
});
describe('validateNodeWithIDInjection', () => {
it('does not crash when is missing children', () => {
const file = program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
</component>
`);
delete file.parser.ast.component;
(0, findNodes_1.validateNodeWithIDInjection)(program);
});
it('does not crash on non-findnode calls', () => {
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
<script uri="ZombieKeyboard.bs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
program.setFile('components/ZombieKeyboard.bs', `
sub init()
print "hello"
m.top.isSameNode(m.top)
end sub
`);
(0, findNodes_1.validateNodeWithIDInjection)(program);
});
});
it('adds assignments to existing init()', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard.bs', `

@@ -73,5 +136,20 @@ sub init()

}));
it('it works when no bs file is present', () => __awaiter(void 0, void 0, void 0, function* () {
it('does not crash when component has no children', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
<component name="ZombieKeyboard" extends="group">
</component>
`);
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]);
yield program.transpile([], stagingDir);
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard.xml`).toString())).to.equal((0, undent_1.default) `
<component name="ZombieKeyboard" extends="group">
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
</component>
`);
}));
it('creates new file when no init() was found', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="group">
<children>

@@ -82,4 +160,31 @@ <label id="helloZombieText" />

`);
const result = yield program.getTranspiledFileContents('components/ZombieKeyboard.bs');
(0, chai_1.expect)(result.code).to.equal((0, undent_1.default) `
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]);
yield program.transpile([], stagingDir);
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()).to.equal(`sub init()\n m.helloZombieText = m.top.findNode("helloZombieText")\nend sub`);
//make sure the import to this new file is present in the xml file
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard.xml`).toString())).to.equal((0, undent_1.default) `
<component name="ZombieKeyboard" extends="group">
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" />
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
}));
it('it works when no init was found', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="group">
<children>
<label id="helloZombieText" />
</children>
</component>
`);
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]);
yield program.transpile([], stagingDir);
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString())).to.equal((0, undent_1.default) `
sub init()

@@ -89,8 +194,38 @@ m.helloZombieText = m.top.findNode("helloZombieText")

`);
//make sure the import to this new file is present in the xml file
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard.xml`).toString())).to.equal((0, undent_1.default) `
<component name="ZombieKeyboard" extends="group">
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" />
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
}));
it('it works when an empty file is present', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard.bs', `
it('it still generates a new file when an empty codebehind file is present', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard.bs', ``);
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="Group">
<script uri="ZombieKeyboard.bs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]);
yield program.transpile([], stagingDir);
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()).to.equal((0, undent_1.default) `
sub init()
m.helloZombieText = m.top.findNode("helloZombieText")
end sub
`);
}));
it('it uses sequence number in generated filename when necessary', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard-findnode.brs', `'original contents`);
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
<component name="ZombieKeyboard" extends="Group">
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" />
<children>

@@ -101,4 +236,8 @@ <label id="helloZombieText" />

`);
const result = yield program.getTranspiledFileContents('components/ZombieKeyboard.bs');
(0, chai_1.expect)(result.code).to.equal((0, undent_1.default) `
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]);
yield program.transpile([], stagingDir);
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()).to.equal((0, undent_1.default) `'original contents`);
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode-2.brs`).toString()).to.equal((0, undent_1.default) `
sub init()

@@ -109,3 +248,24 @@ m.helloZombieText = m.top.findNode("helloZombieText")

}));
it('it works when an file is present with an empty init function', () => __awaiter(void 0, void 0, void 0, function* () {
it('it uses sequence number in generated filename when necessary', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard-findnode.bs', `'original contents`);
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="Group">
<script uri="pkg:/components/ZombieKeyboard-findnode.bs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]);
yield program.transpile([], stagingDir);
(0, chai_1.expect)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()).to.equal((0, undent_1.default) `'original contents`);
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard-findnode-2.brs`).toString())).to.equal((0, undent_1.default) `
sub init()
m.helloZombieText = m.top.findNode("helloZombieText")
end sub
`);
}));
it('it works when a file is present with an empty init function', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/ZombieKeyboard.bs', `

@@ -117,2 +277,3 @@ sub init()

<component name="ZombieKeyboard">
<script uri="ZombieKeyboard.bs" />
<children>

@@ -208,5 +369,22 @@ <label id="helloZombieText" />

});
it('it works when you extend a component and founds nodes are declared within their correct component', () => __awaiter(void 0, void 0, void 0, function* () {
it('it does not warn for findnode IDs NOT in the current xml file', () => {
program.setFile('components/ZombieKeyboard.bs', `
sub init()
m.helloZombieText2 = m.top.findNode("notInMyXml")
end sub
`);
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="Group">
<script uri="ZombieKeyboard.bs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
program.validate();
(0, chai_1.expect)(program.getDiagnostics().map(x => ({ message: x.message, range: x.range, relatedInformation: x.relatedInformation }))).to.eql([]);
});
it('it works when you extend a component and found nodes are declared within their correct component', () => __awaiter(void 0, void 0, void 0, function* () {
program.setFile('components/BaseKeyboard.xml', `
<component name="BaseKeyboard">
<component name="BaseKeyboard" extends="group">
<children>

@@ -224,2 +402,3 @@ <label id="helloText" />

<component name="ZombieKeyboard" extends="BaseKeyboard">
<script uri="ZombieKeyboard.bs" />
<children>

@@ -229,4 +408,7 @@ </children>

`);
let result = yield program.getTranspiledFileContents('components/BaseKeyboard.bs');
(0, chai_1.expect)(result.code).to.equal((0, undent_1.default) `
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
(0, chai_1.expect)(program.getDiagnostics().map(x => x.message)).to.eql([]);
yield program.transpile([], stagingDir);
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/BaseKeyboard-findnode.brs`).toString())).to.equal((0, undent_1.default) `
sub init()

@@ -236,4 +418,3 @@ m.helloText = m.top.findNode("helloText")

`);
result = yield program.getTranspiledFileContents('components/ZombieKeyboard.bs');
(0, chai_1.expect)(result.code).to.equal((0, undent_1.default) `
(0, chai_1.expect)((0, undent_1.default)(fsExtra.readFileSync((0, brighterscript_1.standardizePath) `${stagingDir}/components/ZombieKeyboard.brs`).toString())).to.equal((0, undent_1.default) `
sub init()

@@ -240,0 +421,0 @@ m.helloText.text = "HELLO ZOMBIE"

{
"name": "bsc-plugin-auto-findnode",
"version": "0.1.0",
"version": "0.1.1",
"description": "A BrighterScript plugin that auto-injects `m.top.findNode()` calls in your component `init()` functions",

@@ -54,2 +54,3 @@ "main": "dist/index.js",

"@types/chai": "^4.3.11",
"@types/fs-extra": "^11.0.4",
"@types/mocha": "^10.0.6",

@@ -56,0 +57,0 @@ "@types/node": "^20.10.7",

@@ -1,2 +0,3 @@

import { Program, util } from 'brighterscript';
import type { XmlFile } from 'brighterscript';
import { Program, util, standardizePath as s, AstEditor } from 'brighterscript';
import { expect } from 'chai';

@@ -6,13 +7,94 @@ import { Plugin } from './Plugin';

import undent from 'undent';
import * as fsExtra from 'fs-extra';
import { findChildrenWithIDs, findNodeWithIDInjection, validateNodeWithIDInjection } from './findNodes';
const tempDir = s`${__dirname}/../.tmp`;
const rootDir = s`${tempDir}/rootDir`;
const stagingDir = s`${tempDir}/stagingDir`;
describe('findnode', () => {
let program: Program;
const rootDir = path.join(__dirname, '../.tmp');
beforeEach(() => {
program = new Program({ rootDir: rootDir });
fsExtra.emptyDirSync(tempDir);
fsExtra.emptyDirSync(rootDir);
fsExtra.emptyDirSync(stagingDir);
program = new Program({
rootDir: rootDir,
stagingDir: stagingDir
});
program.plugins.add(new Plugin());
});
it('it works when a bs file is present', async () => {
afterEach(() => {
fsExtra.removeSync(tempDir);
});
describe('findChildrenWithIDs', () => {
it('does not crash on undefined children array', () => {
expect(
Object.entries(findChildrenWithIDs(undefined as any))
).to.eql([]);
});
it('does not crash when child is missing id prop', () => {
expect(
Object.entries(findChildrenWithIDs([{} as any]))
).to.eql([]);
});
it('does not crash when child is missing id prop', () => {
expect(
Object.entries(findChildrenWithIDs([{
id: 1
} as any]))
).to.eql([]);
});
});
describe('findNodeWithIDInjection', () => {
it('does not crash when is missing children', () => {
const file = program.setFile<XmlFile>('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
</component>
`);
delete file.parser.ast.component;
findNodeWithIDInjection(program, [], new AstEditor(), []);
});
});
describe('validateNodeWithIDInjection', () => {
it('does not crash when is missing children', () => {
const file = program.setFile<XmlFile>('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
</component>
`);
delete file.parser.ast.component;
validateNodeWithIDInjection(program);
});
it('does not crash on non-findnode calls', () => {
program.setFile<XmlFile>('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
<script uri="ZombieKeyboard.bs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
program.setFile('components/ZombieKeyboard.bs', `
sub init()
print "hello"
m.top.isSameNode(m.top)
end sub
`);
validateNodeWithIDInjection(program);
});
});
it('adds assignments to existing init()', async () => {
program.setFile('components/ZombieKeyboard.bs', `

@@ -42,5 +124,28 @@ sub init()

it('it works when no bs file is present', async () => {
it('does not crash when component has no children', async () => {
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
<component name="ZombieKeyboard" extends="group">
</component>
`);
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
expect(program.getDiagnostics().map(x => x.message)).to.eql([]);
await program.transpile([], stagingDir);
expect(
undent(
fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard.xml`).toString()
)
).to.equal(undent`
<component name="ZombieKeyboard" extends="group">
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
</component>
`);
});
it('creates new file when no init() was found', async () => {
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="group">
<children>

@@ -52,4 +157,42 @@ <label id="helloZombieText" />

const result = await program.getTranspiledFileContents('components/ZombieKeyboard.bs');
expect(result.code).to.equal(undent`
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
expect(program.getDiagnostics().map(x => x.message)).to.eql([]);
await program.transpile([], stagingDir);
expect(
fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()
).to.equal(`sub init()\n m.helloZombieText = m.top.findNode("helloZombieText")\nend sub`);
//make sure the import to this new file is present in the xml file
expect(
undent(fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard.xml`).toString())
).to.equal(undent`
<component name="ZombieKeyboard" extends="group">
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" />
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
});
it('it works when no init was found', async () => {
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="group">
<children>
<label id="helloZombieText" />
</children>
</component>
`);
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
expect(program.getDiagnostics().map(x => x.message)).to.eql([]);
await program.transpile([], stagingDir);
expect(
undent(fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString())
).to.equal(undent`
sub init()

@@ -59,10 +202,49 @@ m.helloZombieText = m.top.findNode("helloZombieText")

`);
//make sure the import to this new file is present in the xml file
expect(
undent(fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard.xml`).toString())
).to.equal(undent`
<component name="ZombieKeyboard" extends="group">
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" type="text/brightscript" />
<script type="text/brightscript" uri="pkg:/source/bslib.brs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
});
it('it works when an empty file is present', async () => {
program.setFile('components/ZombieKeyboard.bs', `
it('it still generates a new file when an empty codebehind file is present', async () => {
program.setFile('components/ZombieKeyboard.bs', ``);
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="Group">
<script uri="ZombieKeyboard.bs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
expect(program.getDiagnostics().map(x => x.message)).to.eql([]);
await program.transpile([], stagingDir);
expect(
fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()
).to.equal(undent`
sub init()
m.helloZombieText = m.top.findNode("helloZombieText")
end sub
`);
});
it('it uses sequence number in generated filename when necessary', async () => {
program.setFile('components/ZombieKeyboard-findnode.brs', `'original contents`);
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard">
<component name="ZombieKeyboard" extends="Group">
<script uri="pkg:/components/ZombieKeyboard-findnode.brs" />
<children>

@@ -74,4 +256,14 @@ <label id="helloZombieText" />

const result = await program.getTranspiledFileContents('components/ZombieKeyboard.bs');
expect(result.code).to.equal(undent`
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
expect(program.getDiagnostics().map(x => x.message)).to.eql([]);
await program.transpile([], stagingDir);
expect(
fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()
).to.equal(undent`'original contents`);
expect(
fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode-2.brs`).toString()
).to.equal(undent`
sub init()

@@ -83,3 +275,33 @@ m.helloZombieText = m.top.findNode("helloZombieText")

it('it works when an file is present with an empty init function', async () => {
it('it uses sequence number in generated filename when necessary', async () => {
program.setFile('components/ZombieKeyboard-findnode.bs', `'original contents`);
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="Group">
<script uri="pkg:/components/ZombieKeyboard-findnode.bs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
expect(program.getDiagnostics().map(x => x.message)).to.eql([]);
await program.transpile([], stagingDir);
expect(
fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode.brs`).toString()
).to.equal(undent`'original contents`);
expect(
undent(fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard-findnode-2.brs`).toString())
).to.equal(undent`
sub init()
m.helloZombieText = m.top.findNode("helloZombieText")
end sub
`);
});
it('it works when a file is present with an empty init function', async () => {
program.setFile('components/ZombieKeyboard.bs', `

@@ -92,2 +314,3 @@ sub init()

<component name="ZombieKeyboard">
<script uri="ZombieKeyboard.bs" />
<children>

@@ -202,5 +425,28 @@ <label id="helloZombieText" />

it('it works when you extend a component and founds nodes are declared within their correct component', async () => {
it('it does not warn for findnode IDs NOT in the current xml file', () => {
program.setFile('components/ZombieKeyboard.bs', `
sub init()
m.helloZombieText2 = m.top.findNode("notInMyXml")
end sub
`);
program.setFile('components/ZombieKeyboard.xml', `
<component name="ZombieKeyboard" extends="Group">
<script uri="ZombieKeyboard.bs" />
<children>
<label id="helloZombieText" />
</children>
</component>
`);
program.validate();
expect(
program.getDiagnostics().map(x => ({ message: x.message, range: x.range, relatedInformation: x.relatedInformation }))
).to.eql([]);
});
it('it works when you extend a component and found nodes are declared within their correct component', async () => {
program.setFile('components/BaseKeyboard.xml', `
<component name="BaseKeyboard">
<component name="BaseKeyboard" extends="group">
<children>

@@ -220,2 +466,3 @@ <label id="helloText" />

<component name="ZombieKeyboard" extends="BaseKeyboard">
<script uri="ZombieKeyboard.bs" />
<children>

@@ -226,4 +473,10 @@ </children>

let result = await program.getTranspiledFileContents('components/BaseKeyboard.bs');
expect(result.code).to.equal(undent`
//for this test, we need to actually run a full build because this file won't exist until after the build
program.validate();
expect(program.getDiagnostics().map(x => x.message)).to.eql([]);
await program.transpile([], stagingDir);
expect(
undent(fsExtra.readFileSync(s`${stagingDir}/components/BaseKeyboard-findnode.brs`).toString())
).to.equal(undent`
sub init()

@@ -234,4 +487,6 @@ m.helloText = m.top.findNode("helloText")

result = await program.getTranspiledFileContents('components/ZombieKeyboard.bs');
expect(result.code).to.equal(undent`
expect(
undent(fsExtra.readFileSync(s`${stagingDir}/components/ZombieKeyboard.brs`).toString())
).to.equal(undent`
sub init()

@@ -238,0 +493,0 @@ m.helloText.text = "HELLO ZOMBIE"

@@ -1,15 +0,13 @@

import type { AstEditor, FunctionStatement, BrsFile, Program, TranspileObj, BscFile, Statement, Range } from 'brighterscript';
import { isBrsFile, Parser, isXmlScope, DiagnosticSeverity, createVisitor, WalkMode, isDottedGetExpression, isVariableExpression, isLiteralString, util } from 'brighterscript';
import type { SGNode } from 'brighterscript/dist/parser/SGTypes';
import type { AstEditor, FunctionStatement, BrsFile, Program, BscFile, Range, TranspileObj, Scope, XmlFile } from 'brighterscript';
import { isBrsFile, Parser, isXmlScope, DiagnosticSeverity, createVisitor, WalkMode, isDottedGetExpression, isVariableExpression, isLiteralString, util, createSGAttribute } from 'brighterscript';
import { SGScript, type SGNode } from 'brighterscript/dist/parser/SGTypes';
function findChildrenWithIDs(children: Array<SGNode>): Map<string, Range> {
export function findChildrenWithIDs(children: Array<SGNode>): Map<string, Range> {
let foundIDs = new Map<string, Range>();
if (children) {
children.forEach(child => {
if (child.id) {
foundIDs.set(child.id, child.attributes.find(x => x.key.text === 'id')?.value?.range ?? util.createRange(0, 0, 0, 100));
}
const subChildren = findChildrenWithIDs(child.children);
foundIDs = new Map([...foundIDs, ...subChildren]);
});
for (const child of children ?? []) {
if (child.id) {
foundIDs.set(child.id, child.attributes?.find?.(x => x.key.text === 'id')?.value?.range ?? util.createRange(0, 0, 0, 100));
}
const subChildren = findChildrenWithIDs(child.children);
foundIDs = new Map([...foundIDs, ...subChildren]);
}

@@ -24,44 +22,40 @@ return foundIDs;

const ids = findChildrenWithIDs(xmlFile.parser.ast.component?.children?.children ?? []);
if (ids.size > 0) {
const scopeFiles: BscFile[] = scope.getOwnFiles();
//find an init function from all the scope's files
let initFunction: FunctionStatement | undefined;
//skip this xml file if there are no nodes with IDs in it
if (ids.size === 0) {
continue;
}
let hasBrsFile = false;
for (const file of scopeFiles) {
if (isBrsFile(file)) {
hasBrsFile = true;
initFunction = file.parser.references.functionStatementLookup.get('init');
if (initFunction) {
break;
}
}
}
//build the list of assignments
const assignments = Array.from(ids).map(([id, range]) => {
return ` m.${id} = m.top.findNode("${id}")`;
}).join('\n');
if (!hasBrsFile) {
createdFiles.push(program.setFile(xmlFile.pkgPath.replace('.xml', '.bs'), ''));
}
const initFunctionText = `sub init()\n${assignments}\nend sub`;
//create an init function if it's missing
if (!initFunction) {
const codeBehindFile = program.getFiles<BrsFile>(xmlFile.possibleCodebehindPkgPaths).find(x => !!x);
initFunction = Parser.parse(`sub init()\nend sub`).statements[0] as FunctionStatement;
if (codeBehindFile) {
editor.arrayPush(codeBehindFile.parser.statements, initFunction);
}
}
const initFunctionInfo = findInitFunction(scope);
if (initFunction) {
//add m variables for every xml component that has an id
// eslint-disable-next-line max-statements-per-line, @typescript-eslint/brace-style
const assignments = Array.from(ids).map(([id, range]) => { return `m.${id} = m.top.findNode("${id}")`; }).join('\n');
const statements = (Parser.parse(`
sub temp()
${assignments}
end sub
`).statements[0] as FunctionStatement).func.body.statements;
//add the assignments to the top of the init function
editor.arrayUnshift(initFunction.func.body.statements, ...statements);
}
//if we found an init function, inject the assignments
if (initFunctionInfo) {
//add the assignments to the top of the init function
editor.arrayUnshift(
initFunctionInfo.initFunction.func.body.statements,
...(Parser.parse(initFunctionText).ast.statements[0] as FunctionStatement).func.body.statements
);
//we don't have an init function, create a new file and insert an empty init function into it
} else {
//get a unique filename for the new file
const pkgPath = getUniqueFilename(xmlFile, program);
//create and add the new file to the program
const brsFileWithInit = program.setFile<BrsFile>(pkgPath, initFunctionText);
createdFiles.push(brsFileWithInit);
//import this file into the current xml file
editor.arrayPush(xmlFile.parser.ast.component!.scripts, new SGScript({
text: 'script'
}, [
createSGAttribute('uri', util.sanitizePkgPath(brsFileWithInit.pkgPath))
]));
}

@@ -78,17 +72,4 @@ }

if (ids.size > 0) {
const scopeFiles: BscFile[] = scope.getOwnFiles();
const { initFunction, file: initFunctionFile } = findInitFunction(scope) ?? {};
let initFunction: FunctionStatement | undefined;
let initFunctionFile: BscFile | undefined;
for (const file of scopeFiles) {
if (isBrsFile(file)) {
initFunction = file.parser.references.functionStatementLookup.get('init');
if (initFunction) {
initFunctionFile = file;
break;
}
}
}
if (initFunction && initFunctionFile) {

@@ -131,1 +112,35 @@ initFunction.func.body.walk(createVisitor({

}
/**
* Find the first function called `init()` across all files in a scope
*/
function findInitFunction(scope: Scope): { file: BscFile; initFunction: FunctionStatement } | undefined {
for (const file of scope.getOwnFiles()) {
if (isBrsFile(file)) {
const initFunction = file.parser.references.functionStatementLookup.get('init');
if (initFunction) {
return {
initFunction: initFunction,
file: file
};
}
}
}
}
/**
* Get a pkgPath for a new brs file that will sit next to the given xml file. This is deterministic,
* so if the file already exists, we'll append the next available number number to the end of the filename to make it unique.
* @param file the xml file that we want to make a new brs file for
* @param program the bsc program (used for file name collision detection)
*/
function getUniqueFilename(file: XmlFile, program: Program) {
let pkgPath = file.pkgPath.replace('.xml', '-findnode');
let sequence = 2;
//try up to 10 times to find a unique filename within the program
while (sequence < 10 && program.hasFile(`${pkgPath}.brs`) || program.hasFile(`${pkgPath}.bs`)) {
pkgPath = file.pkgPath.replace('.xml', `-findnode-${sequence++}`);
}
return `${pkgPath}.brs`;
}

@@ -23,2 +23,2 @@ import type { AstEditor, BeforeFileValidateEvent, BscFile, CompilerPlugin, PluginHandler, Program, TranspileObj } from 'brighterscript';

}
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc