rooibos-roku 5.16.6 → 5.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
 
8
8
 
9
+ ## [5.17.0](https://github.com/rokucommunity/rooibos/compare/5.16.6...v5.17.0) - 2026-10-06
10
+ ### Added
11
+ - Add Roku Cloud Emulator (RCE) support via CLI ([#447](https://github.com/rokucommunity/rooibos/pull/447))
12
+ ### Changed
13
+ - Istanbul HTML code coverage reports and improved coverage accuracy ([#275](https://github.com/rokucommunity/rooibos/pull/275))
14
+ - Security enhancements ([#444](https://github.com/rokucommunity/rooibos/pull/444), [#446](https://github.com/rokucommunity/rooibos/pull/446))
15
+
16
+
17
+
9
18
  ## [5.16.6](https://github.com/rokucommunity/rooibos/compare/5.16.5...v5.16.6) - 2026-10-01
10
19
  ### Changed
11
20
  - Process every test suite in a file, not just the first ([#441](https://github.com/rokucommunity/rooibos/pull/441))
package/audit-ci.jsonc CHANGED
@@ -3,5 +3,13 @@
3
3
  // why each allowlisted entry is safe to defer.
4
4
  "$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
5
5
  "high": true,
6
- "allowlist": []
6
+ "allowlist": [
7
+ {
8
+ "GHSA-vfj7-8cjw-p6xm": {
9
+ // Added: 2026-10-03
10
+ "active": true,
11
+ "notes": "`braces` <=3.0.3 (prod, via `brighterscript`/`roku-debug`/`roku-deploy`/`fast-glob`): stack exhaustion on deeply nested brace patterns. No patched version is published, so it cannot be upgraded or overridden. The only patterns reaching it are globs from the caller's own project config; no untrusted party supplies them, and a crash only affects the caller's own local process."
12
+ }
13
+ }
14
+ ]
7
15
  }
package/dist/cli.js CHANGED
@@ -1,12 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __rest = (this && this.__rest) || function (s, e) {
4
+ var t = {};
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
6
+ t[p] = s[p];
7
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
8
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
9
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
10
+ t[p[i]] = s[p[i]];
11
+ }
12
+ return t;
13
+ };
3
14
  Object.defineProperty(exports, "__esModule", { value: true });
4
15
  const roku_debug_1 = require("roku-debug");
5
16
  const brighterscript_1 = require("brighterscript");
6
17
  const yargs = require("yargs");
7
18
  const roku_deploy_1 = require("roku-deploy");
8
19
  const fs = require("fs");
20
+ const fsExtra = require("fs-extra");
9
21
  const path = require("path");
22
+ const RooibosConfig_1 = require("./lib/rooibos/RooibosConfig");
23
+ const resolveCliDevice_1 = require("./lib/rooibos/resolveCliDevice");
24
+ const CoverageReporter_1 = require("./lib/rooibos/CoverageReporter");
10
25
  /**
11
26
  * Load simple `KEY=value` pairs from a .env file into process.env, without
12
27
  * overwriting variables that are already set in the real environment.
@@ -33,25 +48,34 @@ function loadDotEnv(envPath = '.env') {
33
48
  }
34
49
  }
35
50
  loadDotEnv();
51
+ // Resolved once by the argument check below, so main() can rely on a valid target and password.
52
+ let password;
53
+ let target;
36
54
  let options = yargs
37
55
  .usage('$0', 'Rooibos: a simple, flexible, fun Brightscript test framework for Roku Scenegraph apps')
38
56
  .help('help', 'View help information about this tool.')
39
57
  .option('project', { type: 'string', description: 'Path to a bsconfig.json project file.' })
40
- .option('host', { type: 'string', description: 'Host of the Roku device to connect to. Overrides value in bsconfig file.' })
41
- .option('password', { type: 'string', description: 'Password of the Roku device to connect to. Overrides value in bsconfig file.' })
58
+ .option('host', { type: 'string', description: 'Host of the Roku device to connect to. Falls back to ROKU_HOST.' })
59
+ .option('esn', { type: 'string', description: 'ESN of a Roku Cloud Emulator device to run on. Requires --token or ROKU_RCE_TOKEN. Cannot be combined with --host or --instance-url.' })
60
+ .option('instance-url', { type: 'string', description: 'URL of a Roku Cloud Emulator instance to run on. Requires --token or ROKU_RCE_TOKEN. Cannot be combined with --host or --esn.' })
61
+ .option('token', { type: 'string', description: 'Roku Cloud Emulator access token (used with --esn or --instance-url). Falls back to the ROKU_RCE_TOKEN environment variable.' })
62
+ .option('password', { type: 'string', description: 'Password of the Roku device to connect to. Falls back to ROKU_PASSWORD.' })
42
63
  .option('log-level', { type: 'string', defaultDescription: '"log"', description: 'The log level. Value can be "error", "warn", "log", "info", "debug".' })
64
+ .option('coverage-dir', { type: 'string', default: './coverage', description: 'Directory to write coverage reports into when codeCoverage is on: lcov.info, coverage-final.json and an html/ report.' })
65
+ .option('code-coverage', { type: 'boolean', description: 'Turn code coverage on (or off with --no-code-coverage) for the CLI\'s build, overriding the rooibos block of the bsconfig. Has no effect with --no-build.' })
66
+ .option('staging-dir', { type: 'string', description: 'Path to the built package directory (staging output). With --no-build this is zipped and deployed as-is; otherwise it overrides where the build stages. Coverage models are read from here.' })
67
+ .option('build', { type: 'boolean', default: true, description: 'Pass --no-build to skip the internal bsc build and deploy an existing staging directory (from --staging-dir or the bsconfig). Assumes it was built with the rooibos plugin so coverage helpers are present.' })
43
68
  .check((argv) => {
44
- var _a;
45
- if (!argv.host && !process.env.ROKU_HOST) {
46
- return new Error('You must provide a host. (--host, or ROKU_HOST in .env)');
47
- }
48
- if (!argv.password && !process.env.ROKU_PASSWORD) {
69
+ var _a, _b;
70
+ password = (_a = argv.password) !== null && _a !== void 0 ? _a : process.env.ROKU_PASSWORD;
71
+ if (!password) {
49
72
  return new Error('You must provide a password. (--password, or ROKU_PASSWORD in .env)');
50
73
  }
74
+ target = (0, resolveCliDevice_1.resolveCliDevice)({ host: argv.host, esn: argv.esn, instanceUrl: argv['instance-url'], token: argv.token }, process.env);
51
75
  if (!argv.project) {
52
76
  console.log('No project file specified. Using "./bsconfig.json"');
53
77
  }
54
- let bsconfigPath = (_a = argv.project) !== null && _a !== void 0 ? _a : './bsconfig.json';
78
+ let bsconfigPath = (_b = argv.project) !== null && _b !== void 0 ? _b : './bsconfig.json';
55
79
  if (!fs.existsSync(bsconfigPath)) {
56
80
  return new Error(`Unable to load ${bsconfigPath}`);
57
81
  }
@@ -59,20 +83,74 @@ let options = yargs
59
83
  })
60
84
  .argv;
61
85
  async function main() {
62
- var _a, _b, _c, _d, _e, _f;
86
+ var _a, _b;
63
87
  let currentErrorCode = 0;
64
88
  let bsconfigPath = (_a = options.project) !== null && _a !== void 0 ? _a : 'bsconfig.json';
65
89
  console.log(`Using bsconfig: ${bsconfigPath}`);
66
90
  const rawConfig = brighterscript_1.util.loadConfigFile(bsconfigPath);
67
91
  const bsConfig = brighterscript_1.util.normalizeConfig(rawConfig);
68
- const host = (_c = (_b = options.host) !== null && _b !== void 0 ? _b : bsConfig.host) !== null && _c !== void 0 ? _c : process.env.ROKU_HOST;
69
- const password = (_e = (_d = options.password) !== null && _d !== void 0 ? _d : bsConfig.password) !== null && _e !== void 0 ? _e : process.env.ROKU_PASSWORD;
70
- const logLevel = (_f = brighterscript_1.LogLevel[options['log-level']]) !== null && _f !== void 0 ? _f : bsConfig.logLevel;
71
- const builder = new brighterscript_1.ProgramBuilder();
72
- builder.logger.logLevel = logLevel;
73
- await builder.run(Object.assign(Object.assign({}, options), { retainStagingDir: true, createPackage: true }));
74
- const device = { host: host };
92
+ const rooibosConfig = (0, RooibosConfig_1.resolveCliRooibosConfig)(rawConfig.rooibos, { codeCoverage: options['code-coverage'] });
93
+ if (options['code-coverage'] !== undefined && options.build === false) {
94
+ console.warn('[rooibos] --code-coverage/--no-code-coverage has no effect with --no-build: the existing package was already built, so its coverage setting is whatever it was built with');
95
+ }
96
+ const { device, label: deviceLabel } = target;
97
+ const logLevel = (_b = brighterscript_1.LogLevel[options['log-level']]) !== null && _b !== void 0 ? _b : bsConfig.logLevel;
98
+ // roku-deploy v4 and roku-debug 0.24 address the target via a device config (resolved
99
+ // by the argument check) rather than a bare `host` string.
75
100
  const rokuDeploy = new roku_deploy_1.RokuDeploy();
101
+ const skipBuild = options.build === false;
102
+ /**
103
+ * Ordered candidate locations for the built package contents: the --staging-dir
104
+ * override, then the bsconfig staging fields, then roku-deploy's default staging
105
+ * location (used when no staging dir is configured anywhere).
106
+ */
107
+ function stagingDirCandidates() {
108
+ const candidates = [];
109
+ if (options['staging-dir']) {
110
+ candidates.push(path.resolve(String(options['staging-dir'])));
111
+ }
112
+ for (const staging of [bsConfig.stagingDir, bsConfig.stagingFolderPath]) {
113
+ if (staging) {
114
+ candidates.push(path.resolve(String(staging)));
115
+ }
116
+ }
117
+ const outDir = bsConfig.outFile ? path.dirname(String(bsConfig.outFile)) : './out';
118
+ candidates.push(path.resolve(outDir, '.roku-deploy-staging'));
119
+ return candidates;
120
+ }
121
+ // Resolved path to the .zip we'll actually deploy when skipping the build. We zip the
122
+ // staging dir into out/rooibos-prebuilt.zip ourselves rather than handing the directory
123
+ // straight to sideload({ dir }), because that path does not forward file patterns and
124
+ // would drop the source-map exclusion below. A directory is required either way - the
125
+ // coverage model (components/rooibos/CodeCoverage.json) must be readable from it.
126
+ let deployZipPath;
127
+ if (skipBuild) {
128
+ const stagingDir = stagingDirCandidates().find((c) => fs.existsSync(c));
129
+ if (!stagingDir) {
130
+ console.error('[rooibos] --no-build requires an existing staging directory: pass --staging-dir or set one in the bsconfig');
131
+ process.exit(1);
132
+ }
133
+ if (!fs.statSync(stagingDir).isDirectory()) {
134
+ console.error(`[rooibos] the staging dir must be a directory, not a file: ${stagingDir}`);
135
+ process.exit(1);
136
+ }
137
+ const zipped = path.resolve('out/rooibos-prebuilt.zip');
138
+ fs.mkdirSync(path.dirname(zipped), { recursive: true });
139
+ console.log(`Zipping pre-built staging dir ${stagingDir} -> ${zipped}`);
140
+ // Exclude source maps - they're useful in the staging dir but shouldn't ship
141
+ // in the package (they bloat channel size and Roku has no use for them).
142
+ // roku-deploy v4 replaced zipFolder(src, out, logger, files) with zip({...});
143
+ // the file patterns still resolve relative to `dir`.
144
+ await rokuDeploy.zip({ dir: stagingDir, out: zipped, files: ['**/*', '!**/*.map'] });
145
+ deployZipPath = zipped;
146
+ }
147
+ else {
148
+ const builder = new brighterscript_1.ProgramBuilder();
149
+ builder.logger.logLevel = logLevel;
150
+ // --staging-dir (if given) flows into bsc as its stagingDir via the spread
151
+ const { token: _token, esn: _esn, 'instance-url': _instanceUrlKebab, instanceUrl: _instanceUrl } = options, buildOptions = __rest(options, ["token", "esn", 'instance-url', "instanceUrl"]);
152
+ await builder.run(Object.assign(Object.assign(Object.assign({}, buildOptions), (rooibosConfig ? { rooibos: rooibosConfig } : {})), { retainStagingDir: true, createPackage: true }));
153
+ }
76
154
  const deviceInfo = await rokuDeploy.getDeviceInfo({ device: device });
77
155
  const rendezvousTracker = new roku_debug_1.RendezvousTracker({ softwareVersion: deviceInfo['software-version'] }, { device: device, remotePort: 8085 });
78
156
  const telnet = new roku_debug_1.TelnetAdapter({ device: device }, rendezvousTracker);
@@ -81,7 +159,52 @@ async function main() {
81
159
  await telnet.connect();
82
160
  const failRegex = /\[Rooibos Result\]: (FAIL|PASS)/g;
83
161
  const endRegex = /\[Rooibos Shutdown\]/g;
162
+ const outputDir = path.resolve(options['coverage-dir']);
163
+ let capturingCounts = false;
164
+ let coverageBuffer = [];
165
+ let coverageReportPromise;
166
+ /**
167
+ * The bsc plugin writes the static coverage model (line/function/branch shape plus
168
+ * repo-relative source paths) into components/rooibos/CodeCoverage.json; read it back
169
+ * from wherever the deployed package contents live.
170
+ */
171
+ function findCoverageModel() {
172
+ for (const dir of stagingDirCandidates()) {
173
+ const candidate = path.join(dir, 'components', 'rooibos', 'CodeCoverage.json');
174
+ const model = (0, CoverageReporter_1.loadCoverageModel)(candidate);
175
+ if (model) {
176
+ console.log(`[rooibos] using coverage model from ${candidate}`);
177
+ return model;
178
+ }
179
+ }
180
+ return undefined;
181
+ }
182
+ /** Dumps the raw device stream into the output dir so a capture is never lost. */
183
+ function saveRawCapture(raw) {
184
+ const rawPath = path.join(outputDir, 'coverage-counts.raw');
185
+ fsExtra.outputFileSync(rawPath, raw);
186
+ console.error(`[rooibos] raw coverage output saved to ${rawPath}`);
187
+ }
188
+ /** The device printed the condensed hit-counts stream. */
189
+ function writeCoverageFromCounts(rawCounts) {
190
+ const model = findCoverageModel();
191
+ if (!model) {
192
+ console.error('[rooibos] the device sent condensed coverage counts but no components/rooibos/CodeCoverage.json was found in the package or staging dir - cannot build coverage reports');
193
+ saveRawCapture(rawCounts);
194
+ return;
195
+ }
196
+ coverageReportPromise = (0, CoverageReporter_1.writeCoverageReportsFromCounts)({
197
+ rawCounts: rawCounts,
198
+ model: model,
199
+ outputDir: outputDir
200
+ }).catch(e => {
201
+ console.error('[rooibos] failed to write coverage reports:', e);
202
+ saveRawCapture(rawCounts);
203
+ });
204
+ }
84
205
  async function doExit(emitAppExit = false) {
206
+ // don't kill the process while coverage reports are still being written
207
+ await coverageReportPromise;
85
208
  if (emitAppExit) {
86
209
  telnet.beginAppExit();
87
210
  }
@@ -90,6 +213,21 @@ async function main() {
90
213
  }
91
214
  telnet.on('console-output', (output) => {
92
215
  console.log(output);
216
+ for (const line of output.split('\n')) {
217
+ if (line.includes('+-=-coverage-counts:start')) {
218
+ capturingCounts = true;
219
+ coverageBuffer = [];
220
+ continue;
221
+ }
222
+ if (line.includes('+-=-coverage-counts:end')) {
223
+ capturingCounts = false;
224
+ writeCoverageFromCounts(coverageBuffer.join('\n'));
225
+ continue;
226
+ }
227
+ if (capturingCounts) {
228
+ coverageBuffer.push(line);
229
+ }
230
+ }
93
231
  //check for Fails or Crashes
94
232
  let failMatches = failRegex.exec(output);
95
233
  if (failMatches && failMatches.length > 0) {
@@ -122,12 +260,15 @@ async function main() {
122
260
  // Actually start the unit tests
123
261
  //deploy a .zip package of your project to a roku device
124
262
  async function deployBuiltFiles() {
125
- const outFile = bsConfig.outFile;
126
- console.log(`Deploying ${outFile} to ${host}`);
263
+ // With --no-build, deploy the staging dir we just zipped; otherwise fall back to the
264
+ // bsconfig-driven outFile that the rooibos build just produced.
265
+ const packagePath = deployZipPath !== null && deployZipPath !== void 0 ? deployZipPath : path.resolve(process.cwd(), bsConfig.outFile);
266
+ console.log(`Deploying ${packagePath} to ${deviceLabel}`);
267
+ // roku-deploy v4 replaced publish({ host, outDir, outFile }) with sideload({ device, zip }).
127
268
  await rokuDeploy.sideload({
128
269
  password: password,
129
270
  device: device,
130
- zip: path.resolve(process.cwd(), outFile)
271
+ zip: packagePath
131
272
  });
132
273
  }
133
274
  await deployBuiltFiles();
@@ -1,44 +1,119 @@
1
- ' @ignore
2
1
  function init()
3
- m.resolvedMap = {}
4
- m.top.observeField("entry", "onEntryChange")
5
- m.top.observeField("save", "onSave")
6
- m.results = []
2
+ ' The coverage map ships as a JSON asset rather than an embedded literal: a real app's
3
+ ' map can exceed Roku's 2MiB-per-.brs-file compile limit (&hb9) when written into code.
4
+ m.coverageMap = ParseJson(ReadAsciiFile("pkg:/components/rooibos/CodeCoverage.json"))
5
+ m.port = createObject("roMessagePort")
6
+ m.top.observeFieldScoped("entry", m.port)
7
+ ' save arrives on the same port so the task thread can block on wait() instead of
8
+ ' polling m.top.save (a cross-thread rendezvous) on a sleep loop
9
+ m.top.observeFieldScoped("save", m.port)
10
+ m.top.functionName = "runTaskThread"
11
+ m.top.control = "RUN"
7
12
  end function
8
13
 
9
- ' @ignore
10
- function setExpectedMap()
11
- m.top.expectedMap = "#EXPECTED_MAP#"
12
- end function
14
+ function runTaskThread() as void
15
+ ' Per-file line lookups keyed by line number, built lazily on first hit (on the task
16
+ ' thread, so the render thread never pays for index construction). The values are
17
+ ' references to the same line AAs as file.lines, so mutating them updates the model.
18
+ m.lineIndexByFile = {}
19
+ ' Entries whose ids don't resolve in the model. Any nonzero count means the package
20
+ ' mixes instrumentation passes (e.g. stale staged files from an earlier build whose
21
+ ' baked-in ids alias different files in this build's CodeCoverage.json) - skip and
22
+ ' tally rather than crash the task.
23
+ m.unmatchedEntries = 0
24
+ while true
25
+ events = []
26
+ saving = false
27
+ ' blocking wait: zero CPU and no cross-thread field reads while idle
28
+ message = wait(0, m.port)
29
+ if message.getField() = "save" then
30
+ saving = (message.getData() = true)
31
+ else
32
+ events.push(message)
33
+ end if
34
+ if saving = true then
35
+ ? "Saving unprocessed code cov events..."
36
+ 'Drain all the unprocessed messages (3ms grace per message, then done)
37
+ while true
38
+ message = wait(3, m.port)
39
+ if message = invalid then
40
+ exit while
41
+ else if message.getField() <> "save" then
42
+ events.push(message)
43
+ end if
44
+ end while
13
45
 
14
- ' @ignore
15
- function setFilePathMap()
16
- m.top.filePathMap = "#FILE_PATH_MAP#"
17
- end function
46
+ ? "Found" events.count() " unprocessed events..."
47
+ end if
18
48
 
19
- ' @ignore
20
- function onEntryChange()
21
- entry = m.top.entry
22
- ' defer till later
23
- m.results.push(entry)
24
- end function
49
+ ' entry.r values below are force-substituted at build time from the TS
50
+ ' CodeCoverageLineType enum (FileFactory.createCoverageComponent keys on the
51
+ ' #LINE_TYPE_*# markers), so producer and consumer cannot drift. The literals
52
+ ' here only matter for parsing this template standalone.
25
53
 
26
- ' @ignore
27
- function onSave()
28
- ? "saving data"
29
- for each entry in m.results
30
- if entry <> invalid then
31
- fileId = entry.f
32
- lineMap = m.resolvedMap[fileId]
54
+ for each event in events
55
+ entry = event.getData()
56
+ if entry <> invalid then
57
+ file = m.coverageMap.files[entry.f]
58
+ if file = invalid then
59
+ m.unmatchedEntries++
60
+ else if entry.r = 4 then ' #LINE_TYPE_FUNCTION#
61
+ func = file.functions[entry.fn]
62
+ if func <> invalid then
63
+ if func.totalHit = 0 then
64
+ file.functionTotalHit++
65
+ end if
66
+ func.totalHit++
67
+ else
68
+ m.unmatchedEntries++
69
+ end if
70
+ else if entry.r = 3 then ' #LINE_TYPE_BRANCH#
71
+ ' branch ids are assigned as array indexes at build time, so index directly
72
+ block = file.blocks[entry.bl]
73
+ branch = invalid
74
+ if block <> invalid then
75
+ branch = block.branches[entry.br]
76
+ end if
77
+ if branch <> invalid then
78
+ if branch.totalHit = 0 then
79
+ file.branchTotalHit++
80
+ end if
81
+ branch.totalHit++
82
+ else
83
+ m.unmatchedEntries++
84
+ end if
85
+ else if entry.r = 1 then ' #LINE_TYPE_CODE#
86
+ fileKey = stri(entry.f).trim()
87
+ lineIndex = m.lineIndexByFile[fileKey]
88
+ if lineIndex = invalid then
89
+ lineIndex = {}
90
+ for each line in file.lines
91
+ lineIndex[stri(line.lineNumber).trim()] = line
92
+ end for
93
+ m.lineIndexByFile[fileKey] = lineIndex
94
+ end if
95
+ line = lineIndex[stri(entry.l).trim()]
96
+ if line <> invalid then
97
+ if line.totalHit = 0 then
98
+ file.lineTotalHit++
99
+ end if
100
+ line.totalHit++
101
+ else
102
+ m.unmatchedEntries++
103
+ end if
104
+ end if
105
+ ' no write-back needed: file and its members are references into m.coverageMap
106
+ end if
107
+ end for
33
108
 
34
- if lineMap = invalid then
35
- lineMap = {}
36
- m.resolvedMap[fileId] = lineMap
109
+ if saving = true then
110
+ if m.unmatchedEntries > 0 then
111
+ ? "[rooibos coverage] WARNING: " ; stri(m.unmatchedEntries).trim() ; " coverage entries did not match the coverage model and were skipped."
112
+ ? "[rooibos coverage] This usually means the package mixes files from different builds - rebuild into a clean staging directory."
37
113
  end if
38
- lineMap[entry.l] = entry.r
114
+ m.top.coverageResults = m.coverageMap
115
+ return
39
116
  end if
40
- end for
41
- m.top.resolvedMap = m.resolvedMap
42
- setExpectedMap()
43
- setFilePathMap()
117
+ end while
118
+
44
119
  end function
@@ -1,11 +1,13 @@
1
- <component name="CodeCoverage" extends="ContentNode">
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <component name="CodeCoverage"
3
+ extends="Task"
4
+ >
2
5
  <script type="text/brightscript" uri="CodeCoverage.brs" />
3
6
 
4
7
  <interface>
5
8
  <field id="entry" type="assocarray" />
9
+ <field id="coverageResults" type="assocarray" />
10
+
6
11
  <field id="save" type="boolean" />
7
- <field id="expectedMap" type="assocarray" />
8
- <field id="resolvedMap" type="assocarray" />
9
- <field id="filePathMap" type="assocarray" />
10
12
  </interface>
11
13
  </component>