• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brs / #310

01 Dec 2023 08:37PM UTC coverage: 91.498% (+1.0%) from 90.458%
#310

push

TwitchBronBron
0.45.3

1784 of 2079 branches covered (85.81%)

Branch coverage included in aggregate %.

5265 of 5625 relevant lines covered (93.6%)

8959.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.47
/src/componentprocessor/index.ts
1
import * as fs from "fs";
132✔
2
import * as path from "path";
132✔
3
import { promisify } from "util";
132✔
4
import { XmlDocument, XmlElement } from "xmldoc";
132✔
5
import pSettle = require("p-settle");
132✔
6
const readFile = promisify(fs.readFile);
132✔
7
import * as fg from "fast-glob";
132✔
8
import { Environment } from "../interpreter/Environment";
9
import { BrsError } from "../Error";
132✔
10

11
interface FieldAttributes {
12
    id: string;
13
    type: string;
14
    alias?: string;
15
    value?: string;
16
    onChange?: string;
17
    alwaysNotify?: string;
18
}
19

20
interface ComponentFields {
21
    [key: string]: FieldAttributes;
22
}
23

24
interface FunctionAttributes {
25
    name: string;
26
}
27

28
interface ComponentFunctions {
29
    [key: string]: FunctionAttributes;
30
}
31

32
interface NodeField {
33
    [id: string]: string;
34
}
35

36
export interface ComponentNode {
37
    name: string;
38
    fields: NodeField;
39
    children: ComponentNode[];
40
}
41

42
export interface ComponentScript {
43
    type: string;
44
    uri: string;
45
}
46

47
export class ComponentDefinition {
132✔
48
    public contents?: string;
49
    public xmlNode?: XmlDocument;
50
    public name?: string;
51
    // indicates whether this component hierarchy has been processed before
52
    // which means the fields, children, and inherited functions are correctly set
53
    public processed: boolean = false;
1,740✔
54
    public fields: ComponentFields = {};
1,740✔
55
    public functions: ComponentFunctions = {};
1,740✔
56
    public children: ComponentNode[] = [];
1,740✔
57
    public scripts: ComponentScript[] = [];
1,740✔
58
    public environment: Environment | undefined;
59

60
    constructor(readonly xmlPath: string) {}
1,740✔
61

62
    async parse(): Promise<ComponentDefinition> {
63
        let contents;
64
        try {
1,738✔
65
            contents = await readFile(this.xmlPath, "utf-8");
1,738✔
66
            let xmlStr = contents.toString().replace(/\r?\n|\r/g, "");
1,738✔
67
            this.xmlNode = new XmlDocument(xmlStr);
1,729✔
68
            this.name = this.xmlNode.attr.name;
1,715✔
69

70
            return Promise.resolve(this);
1,715✔
71
        } catch (err) {
72
            // TODO: provide better parse error reporting
73
            //   cases:
74
            //     * file read error
75
            //     * XML parse error
76
            return Promise.reject(this);
23✔
77
        }
78
    }
79

80
    public get extends(): string {
81
        return this.xmlNode ? this.xmlNode.attr.extends : "";
3,067!
82
    }
83
}
84

85
export async function getComponentDefinitionMap(
132✔
86
    rootDir: string = "",
2✔
87
    additionalDirs: string[] = [],
9✔
88
    libraryName: string | undefined
89
) {
90
    let searchString = "{components, }";
117✔
91
    if (additionalDirs.length) {
117✔
92
        searchString = `{components,${additionalDirs.join(",")}}`;
1✔
93
    }
94
    const componentsPattern = path.join(rootDir, searchString, "**", "*.xml");
117✔
95
    const xmlFiles: string[] = fg.sync(componentsPattern, {});
117✔
96

97
    let defs = xmlFiles.map((file) => new ComponentDefinition(file));
1,736✔
98
    let parsedPromises = defs.map(async (def) => def.parse());
1,736✔
99

100
    return processXmlTree(pSettle(parsedPromises), rootDir, libraryName);
117✔
101
}
102

103
async function processXmlTree(
104
    settledPromises: Promise<pSettle.SettledResult<ComponentDefinition>[]>,
105
    rootDir: string,
106
    libraryName: string | undefined
107
) {
108
    let nodeDefs = await settledPromises;
117✔
109
    let nodeDefMap = new Map<string, ComponentDefinition>();
117✔
110

111
    // create map of just ComponentDefinition objects
112
    nodeDefs.map((item) => {
117✔
113
        if (item.isFulfilled && !item.isRejected) {
1,736✔
114
            let name = item.value?.name?.toLowerCase();
1,714!
115
            if (libraryName) {
1,714✔
116
                name = `${libraryName.toLowerCase()}:${name}`;
1✔
117
            }
118
            if (name) {
1,714✔
119
                nodeDefMap.set(name, item.value!);
1,714✔
120
            }
121
        }
122
    });
123

124
    // recursively create an inheritance stack for each component def and build up
125
    // the component backwards from most extended component first
126
    let inheritanceStack: ComponentDefinition[] = [];
117✔
127

128
    nodeDefMap.forEach((nodeDef) => {
117✔
129
        if (nodeDef && nodeDef.processed === false) {
1,714✔
130
            let xmlNode = nodeDef.xmlNode;
1,546✔
131
            inheritanceStack.push(nodeDef);
1,546✔
132
            //builds inheritance stack
133
            while (xmlNode && xmlNode.attr.extends) {
1,546✔
134
                let superNodeDef = nodeDefMap.get(xmlNode.attr.extends?.toLowerCase());
403!
135
                if (superNodeDef) {
403✔
136
                    inheritanceStack.push(superNodeDef);
289✔
137
                    xmlNode = superNodeDef.xmlNode;
289✔
138
                } else {
139
                    xmlNode = undefined;
114✔
140
                }
141
            }
142

143
            let inheritedFunctions: ComponentFunctions = {};
1,546✔
144
            // pop the stack & build our component
145
            // we can safely assume nodes are valid ComponentDefinition objects
146
            while (inheritanceStack.length > 0) {
1,546✔
147
                let newNodeDef = inheritanceStack.pop();
1,835✔
148
                if (newNodeDef) {
1,835✔
149
                    if (newNodeDef.processed) {
1,835✔
150
                        inheritedFunctions = newNodeDef.functions;
121✔
151
                    } else {
152
                        let nodeInterface = processInterface(newNodeDef.xmlNode!);
1,714✔
153
                        inheritedFunctions = { ...inheritedFunctions, ...nodeInterface.functions };
1,714✔
154

155
                        // Use inherited functions in children so that we can correctly find functions in callFunc.
156
                        newNodeDef.functions = inheritedFunctions;
1,714✔
157
                        newNodeDef.fields = nodeInterface.fields;
1,714✔
158
                        newNodeDef.processed = true;
1,714✔
159
                    }
160
                }
161
            }
162
        }
163
    });
164

165
    for (let nodeDef of nodeDefMap.values()) {
117✔
166
        let xmlNode = nodeDef.xmlNode;
1,714✔
167
        if (xmlNode) {
1,714✔
168
            nodeDef.children = getChildren(xmlNode);
1,714✔
169
            nodeDef.scripts = await getScripts(xmlNode, nodeDef.xmlPath, rootDir);
1,714✔
170
        }
171
    }
172

173
    return nodeDefMap;
117✔
174
}
175

176
/**
177
 * Returns all the fields and functions found in the Xml node.
178
 * @param node Xml node with fields
179
 * @return { fields, functions }: the fields and functions parsed as
180
 * ComponentFields and ComponentFunctions respectively
181
 */
182
function processInterface(node: XmlDocument): {
183
    fields: ComponentFields;
184
    functions: ComponentFunctions;
185
} {
186
    let iface = node.childNamed("interface");
1,714✔
187
    let fields: ComponentFields = {};
1,714✔
188
    let functions: ComponentFunctions = {};
1,714✔
189

190
    if (!iface) {
1,714✔
191
        return { fields, functions };
641✔
192
    }
193

194
    iface.eachChild((child) => {
1,073✔
195
        if (child.name === "field") {
1,800✔
196
            fields[child.attr.id] = {
1,240✔
197
                type: child.attr.type,
198
                id: child.attr.id,
199
                alias: child.attr.alias,
200
                onChange: child.attr.onChange,
201
                alwaysNotify: child.attr.alwaysNotify,
202
                value: child.attr.value,
203
            };
204
        } else if (child.name === "function") {
560✔
205
            functions[child.attr.name] = {
560✔
206
                name: child.attr.name,
207
            };
208
        }
209
    });
210

211
    return { fields, functions };
1,073✔
212
}
213

214
/**
215
 * Given a node as a XmlDocument it will get all the children and return
216
 * them parsed.
217
 * @param node The XmlDocument that has the children.
218
 * @returns The parsed children
219
 */
220
function getChildren(node: XmlDocument): ComponentNode[] {
221
    let xmlElement = node.childNamed("children");
1,714✔
222

223
    if (!xmlElement) {
1,714✔
224
        return [];
810✔
225
    }
226

227
    let children: ComponentNode[] = [];
904✔
228
    parseChildren(xmlElement, children);
904✔
229

230
    return children;
904✔
231
}
232

233
/**
234
 * Parses children in the XmlElement converting then into an object
235
 * that follows the ComponentNode interface. This process makes
236
 * the tree creation simpler.
237
 * @param element The XmlElement that has the children to be parsed
238
 * @param children The array where parsed children will be added
239
 */
240
function parseChildren(element: XmlElement, children: ComponentNode[]): void {
241
    element.eachChild((child) => {
1,464✔
242
        let childComponent: ComponentNode = {
1,913✔
243
            name: child.name,
244
            fields: child.attr,
245
            children: [],
246
        };
247

248
        if (child.children.length > 0) {
1,913✔
249
            parseChildren(child, childComponent.children);
560✔
250
        }
251

252
        children.push(childComponent);
1,913✔
253
    });
254
}
255

256
async function getScripts(
257
    node: XmlDocument,
258
    xmlPath: string,
259
    rootDir: string
260
): Promise<ComponentScript[]> {
261
    let scripts = node.childrenNamed("script");
1,714✔
262
    let componentScripts: ComponentScript[] = [];
1,714✔
263

264
    for (let script of scripts) {
1,714✔
265
        let absoluteUri: URL;
266
        try {
1,668✔
267
            absoluteUri = new URL(script.attr.uri, `pkg:/${path.posix.relative(rootDir, xmlPath)}`);
1,668✔
268
        } catch (err) {
269
            let file = await readFile(xmlPath, "utf-8");
×
270

271
            let tag = file.substring(script.startTagPosition, script.position);
×
272
            let tagLines = tag.split("\n");
×
273
            let leadingLines = file.substring(0, script.startTagPosition).split("\n");
×
274
            let start = {
×
275
                line: leadingLines.length,
276
                column: columnsInLastLine(leadingLines),
277
            };
278

279
            return Promise.reject({
×
280
                message: BrsError.format(
281
                    `Invalid path '${script.attr.uri}' found in <script/> tag`,
282
                    {
283
                        file: xmlPath,
284
                        start: start,
285
                        end: {
286
                            line: start.line + tagLines.length - 1,
287
                            column: start.column + columnsInLastLine(tagLines),
288
                        },
289
                    }
290
                ).trim(),
291
            });
292
        }
293

294
        if (script.attr) {
1,668✔
295
            componentScripts.push({
1,668✔
296
                type: script.attr.type,
297
                uri: absoluteUri.href,
298
            });
299
        }
300
    }
301

302
    return componentScripts;
1,714✔
303
}
304

305
/**
306
 * Returns the number of columns occupied by the final line in an array of lines as parsed by `xmldoc`.
307
 * xmldoc parses positions to ignore `\n` characters, which is pretty confusing.  This function
308
 * compensates for that.
309
 *
310
 * @param lines an array of strings, where each is a line from an XML document
311
 *
312
 * @return the corrected column number for the last line of text as parsed by `xmlDoc`
313
 */
314
function columnsInLastLine(lines: string[]): number {
315
    return lines[lines.length - 1].length + lines.length - 1;
×
316
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc