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

rokucommunity / roku-debug / #2026

02 Nov 2022 02:41PM UTC coverage: 56.194% (+7.0%) from 49.18%
#2026

push

TwitchBronBron
0.17.0

997 of 1898 branches covered (52.53%)

Branch coverage included in aggregate %.

2074 of 3567 relevant lines covered (58.14%)

15.56 hits per line

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

85.89
/src/managers/ProjectManager.ts
1
import * as assert from 'assert';
1✔
2
import * as fsExtra from 'fs-extra';
1✔
3
import * as path from 'path';
1✔
4
import { rokuDeploy, RokuDeploy } from 'roku-deploy';
1✔
5
import type { FileEntry } from 'roku-deploy';
6
import * as glob from 'glob';
1✔
7
import { promisify } from 'util';
1✔
8
const globAsync = promisify(glob);
1✔
9
import type { BreakpointManager } from './BreakpointManager';
10
import { fileUtils, standardizePath as s } from '../FileUtils';
1✔
11
import type { LocationManager, SourceLocation } from './LocationManager';
12
import { util } from '../util';
1✔
13
import { logger } from '../logging';
1✔
14

15
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
16
const replaceInFile = require('replace-in-file');
1✔
17

18
export const componentLibraryPostfix = '__lib';
1✔
19

20
/**
21
 * Manages the collection of brightscript projects being used in a debug session.
22
 * Will contain the main project (in rootDir), as well as component libraries.
23
 */
24
export class ProjectManager {
1✔
25
    public constructor(
26
        /**
27
         * A class that keeps track of all the breakpoints for a debug session.
28
         * It needs to be notified of any changes in breakpoints
29
         */
30
        public breakpointManager: BreakpointManager,
66✔
31
        public locationManager: LocationManager
66✔
32
    ) {
33
        this.breakpointManager = breakpointManager;
66✔
34
    }
35

36
    public launchConfiguration: {
37
        enableSourceMaps?: boolean;
38
        enableDebugProtocol?: boolean;
39
    };
40

41
    public mainProject: Project;
42
    public componentLibraryProjects = [] as ComponentLibraryProject[];
66✔
43

44
    public addComponentLibraryProject(project: ComponentLibraryProject) {
45
        this.componentLibraryProjects.push(project);
66✔
46
    }
47

48
    public getAllProjects() {
49
        return [
×
50
            ...(this.mainProject ? [this.mainProject] : []),
×
51
            ...(this.componentLibraryProjects ?? [])
×
52
        ];
53
    }
54

55
    /**
56
     * Get the list of staging folder paths from all projects
57
     */
58
    public getStagingFolderPaths() {
59
        let projects = [
1✔
60
            ...(this.mainProject ? [this.mainProject] : []),
1!
61
            ...(this.componentLibraryProjects ?? [])
3!
62
        ];
63
        return projects.map(x => x.stagingFolderPath);
2✔
64
    }
65

66
    /**
67
     * Given a debugger path and line number, compensate for the injected breakpoint line offsets
68
     * @param filePath - the path to the file that may or may not have breakpoints
69
     * @param debuggerLineNumber - the line number from the debugger
70
     */
71
    public getLineNumberOffsetByBreakpoints(filePath: string, debuggerLineNumber: number) {
72
        let breakpoints = this.breakpointManager.getPermanentBreakpointsForFile(filePath);
26✔
73
        //throw out duplicate breakpoints (account for entry breakpoint) and sort them ascending
74
        breakpoints = this.breakpointManager.sortAndRemoveDuplicateBreakpoints(breakpoints);
26✔
75

76
        let sourceLineByDebuggerLine = {};
26✔
77
        let sourceLineNumber = 0;
26✔
78
        for (let loopDebuggerLineNumber = 1; loopDebuggerLineNumber <= debuggerLineNumber; loopDebuggerLineNumber++) {
26✔
79
            sourceLineNumber++;
210✔
80
            sourceLineByDebuggerLine[loopDebuggerLineNumber] = sourceLineNumber;
210✔
81

82
            /**
83
             * A line with a breakpoint on it should share the same debugger line number.
84
             * The injected `STOP` line will be given the correct line number automatically,
85
             * but we need to compensate for the actual code line. So if there's a breakpoint
86
             * on this line, handle the next line's mapping as well (and skip one iteration of the loop)
87
             */
88
            // eslint-disable-next-line @typescript-eslint/no-loop-func
89
            let breakpointForLine = breakpoints.find(x => x.line === sourceLineNumber);
673✔
90
            if (breakpointForLine) {
210✔
91
                sourceLineByDebuggerLine[loopDebuggerLineNumber + 1] = sourceLineNumber;
79✔
92
                loopDebuggerLineNumber++;
79✔
93
            }
94
        }
95

96
        return sourceLineByDebuggerLine[debuggerLineNumber];
26✔
97
    }
98

99
    /**
100
     * @param debuggerPath
101
     * @param debuggerLineNumber - the 1-based line number from the debugger
102
     */
103
    public async getSourceLocation(debuggerPath: string, debuggerLineNumber: number) {
104
        //get source location using
105
        let stagingFileInfo = await this.getStagingFileInfo(debuggerPath);
10✔
106
        if (!stagingFileInfo) {
10✔
107
            return;
1✔
108
        }
109
        let project = stagingFileInfo.project;
9✔
110

111
        //remove the component library postfix if present
112
        if (project instanceof ComponentLibraryProject) {
9✔
113
            stagingFileInfo.absolutePath = fileUtils.unPostfixFilePath(stagingFileInfo.absolutePath, project.postfix);
1✔
114
            stagingFileInfo.relativePath = fileUtils.unPostfixFilePath(stagingFileInfo.relativePath, project.postfix);
1✔
115
        }
116

117
        let sourceLocation = await this.locationManager.getSourceLocation({
9✔
118
            lineNumber: debuggerLineNumber,
119
            columnIndex: 0,
120
            fileMappings: project.fileMappings,
121
            rootDir: project.rootDir,
122
            stagingFilePath: stagingFileInfo.absolutePath,
123
            stagingFolderPath: project.stagingFolderPath,
124
            sourceDirs: project.sourceDirs,
125
            enableSourceMaps: this.launchConfiguration?.enableSourceMaps ?? true
54!
126
        });
127

128
        //if sourcemaps are disabled, and this is a telnet debug dession, account for breakpoint offsets
129
        if (sourceLocation && this.launchConfiguration?.enableSourceMaps === false && !this.launchConfiguration.enableDebugProtocol) {
9!
130
            sourceLocation.lineNumber = this.getLineNumberOffsetByBreakpoints(sourceLocation.filePath, sourceLocation.lineNumber);
×
131
        }
132

133
        if (!sourceLocation?.filePath) {
9✔
134
            //couldn't find a source location. At least send back the staging file information so the user can still debug
135
            return {
4✔
136
                filePath: stagingFileInfo.absolutePath,
137
                lineNumber: sourceLocation?.lineNumber || debuggerLineNumber,
20!
138
                columnIndex: 0
139
            } as SourceLocation;
140
        } else {
141
            return sourceLocation;
5✔
142
        }
143
    }
144

145
    /**
146
     *
147
     * @param stagingFolderPath - the path to
148
     */
149
    public async registerEntryBreakpoint(stagingFolderPath: string) {
150
        //find the main function from the staging flder
151
        let entryPoint = await fileUtils.findEntryPoint(stagingFolderPath);
×
152

153
        //convert entry point staging location to source location
154
        let sourceLocation = await this.getSourceLocation(entryPoint.relativePath, entryPoint.lineNumber);
×
155

156
        //register the entry breakpoint
157
        this.breakpointManager.setBreakpoint(sourceLocation.filePath, {
×
158
            //+1 to select the first line of the function
159
            line: sourceLocation.lineNumber + 1
160
        });
161
    }
162

163
    /**
164
     * Given a debugger-relative file path, find the path to that file in the staging directory.
165
     * This supports the standard out dir, as well as component library out dirs
166
     * @param debuggerPath the path to the file which was provided by the debugger
167
     * @param stagingFolderPath - the path to the root of the staging folder (where all of the files were copied before deployment)
168
     * @return a full path to the file in the staging directory
169
     */
170
    public async getStagingFileInfo(debuggerPath: string) {
171
        let project: Project;
172

173
        let componentLibraryIndex = fileUtils.getComponentLibraryIndexFromFileName(debuggerPath, componentLibraryPostfix);
14✔
174
        //component libraries
175
        if (componentLibraryIndex !== undefined) {
14✔
176
            let lib = this.componentLibraryProjects.find(x => x.libraryIndex === componentLibraryIndex);
3✔
177
            if (lib) {
3!
178
                project = lib;
3✔
179
            } else {
180
                throw new Error(`There is no component library with index ${componentLibraryIndex}`);
×
181
            }
182
            //standard project files
183
        } else {
184
            project = this.mainProject;
11✔
185
        }
186

187
        let relativePath: string;
188

189
        //if the path starts with a scheme (i.e. pkg:/ or complib:/, we have an exact match.
190
        if (util.getFileScheme(debuggerPath)) {
14✔
191
            relativePath = util.removeFileScheme(debuggerPath);
8✔
192
        } else {
193
            relativePath = await fileUtils.findPartialFileInDirectory(debuggerPath, project.stagingFolderPath);
6✔
194
        }
195
        if (relativePath) {
14✔
196
            relativePath = fileUtils.removeLeadingSlash(
13✔
197
                fileUtils.standardizePath(relativePath
198
                )
199
            );
200
            return {
13✔
201
                relativePath: relativePath,
202
                absolutePath: s`${project.stagingFolderPath}/${relativePath}`,
203
                project: project
204
            };
205
        } else {
206
            return undefined;
1✔
207
        }
208
    }
209
}
210

211
export interface AddProjectParams {
212
    rootDir: string;
213
    outDir: string;
214
    sourceDirs?: string[];
215
    files: Array<FileEntry>;
216
    injectRaleTrackerTask?: boolean;
217
    raleTrackerTaskFileLocation?: string;
218
    injectRdbOnDeviceComponent?: boolean;
219
    rdbFilesBasePath?: string;
220
    bsConst?: Record<string, boolean>;
221
    stagingFolderPath?: string;
222
}
223

224
export class Project {
1✔
225
    constructor(params: AddProjectParams) {
226
        assert(params?.rootDir, 'rootDir is required');
135!
227
        this.rootDir = fileUtils.standardizePath(params.rootDir);
135✔
228

229
        assert(params?.outDir, 'outDir is required');
135!
230
        this.outDir = fileUtils.standardizePath(params.outDir);
135✔
231

232
        this.stagingFolderPath = params.stagingFolderPath ?? rokuDeploy.getOptions(this).stagingFolderPath;
135✔
233
        this.bsConst = params.bsConst;
135✔
234
        this.sourceDirs = (params.sourceDirs ?? [])
135✔
235
            //standardize every sourcedir
236
            .map(x => fileUtils.standardizePath(x));
25✔
237
        this.injectRaleTrackerTask = params.injectRaleTrackerTask ?? false;
135✔
238
        this.raleTrackerTaskFileLocation = params.raleTrackerTaskFileLocation;
135✔
239
        this.injectRdbOnDeviceComponent = params.injectRdbOnDeviceComponent ?? false;
135✔
240
        this.rdbFilesBasePath = params.rdbFilesBasePath;
135✔
241
        this.files = params.files ?? [];
135✔
242
    }
243
    public rootDir: string;
244
    public outDir: string;
245
    public sourceDirs: string[];
246
    public files: Array<FileEntry>;
247
    public stagingFolderPath: string;
248
    public fileMappings: Array<{ src: string; dest: string }>;
249
    public bsConst: Record<string, boolean>;
250
    public injectRaleTrackerTask: boolean;
251
    public raleTrackerTaskFileLocation: string;
252
    public injectRdbOnDeviceComponent: boolean;
253
    public rdbFilesBasePath: string;
254

255
    //the default project doesn't have a postfix, but component libraries will have a postfix, so just use empty string to standardize the postfix logic
256
    public get postfix() {
257
        return '';
18✔
258
    }
259

260
    private logger = logger.createLogger(`[${ProjectManager.name}]`);
135✔
261

262
    public async stage() {
263
        let rd = new RokuDeploy();
8✔
264
        if (!this.fileMappings) {
8✔
265
            this.fileMappings = await this.getFileMappings();
3✔
266
        }
267

268
        //override the getFilePaths function so rokuDeploy doesn't run it again during prepublishToStaging
269
        (rd as any).getFilePaths = () => {
8✔
270
            let relativeFileMappings = [];
8✔
271
            for (let fileMapping of this.fileMappings) {
8✔
272
                relativeFileMappings.push({
9✔
273
                    src: fileMapping.src,
274
                    dest: fileUtils.replaceCaseInsensitive(fileMapping.dest, this.stagingFolderPath, '')
275
                });
276
            }
277
            return Promise.resolve(relativeFileMappings);
8✔
278
        };
279

280
        //copy all project files to the staging folder
281
        await rd.prepublishToStaging({
8✔
282
            rootDir: this.rootDir,
283
            stagingFolderPath: this.stagingFolderPath,
284
            files: this.files,
285
            outDir: this.outDir
286
        });
287

288
        //preload the original location of every file
289
        await this.resolveFileMappingsForSourceDirs();
8✔
290

291
        await this.transformManifestWithBsConst();
8✔
292

293
        await this.copyAndTransformRaleTrackerTask();
8✔
294

295
        await this.copyAndTransformRDB();
8✔
296
    }
297

298
    /**
299
     * If the project uses sourceDirs, replace every `fileMapping.src` with its original location in sourceDirs
300
     */
301
    private resolveFileMappingsForSourceDirs() {
302
        return Promise.all([
8✔
303
            this.fileMappings.map(async x => {
304
                let stagingFilePathRelative = fileUtils.getRelativePath(this.stagingFolderPath, x.dest);
9✔
305
                let sourceDirFilePath = await fileUtils.findFirstRelativeFile(stagingFilePathRelative, this.sourceDirs);
9✔
306
                if (sourceDirFilePath) {
9!
307
                    x.src = sourceDirFilePath;
×
308
                }
309
            })
310
        ]);
311
    }
312

313
    /**
314
     * Apply the bsConst transformations to the manifest file for this project
315
     */
316
    public async transformManifestWithBsConst() {
317
        if (this.bsConst) {
8✔
318
            let manifestPath = s`${this.stagingFolderPath}/manifest`;
1✔
319
            if (await fsExtra.pathExists(manifestPath)) {
1!
320
                // Update the bs_const values in the manifest in the staging folder before side loading the channel
321
                let fileContents = (await fsExtra.readFile(manifestPath)).toString();
1✔
322
                fileContents = this.updateManifestBsConsts(this.bsConst, fileContents);
1✔
323
                await fsExtra.writeFile(manifestPath, fileContents);
1✔
324
            }
325
        }
326
    }
327

328
    public updateManifestBsConsts(consts: Record<string, boolean>, fileContents: string): string {
329
        let bsConstLine: string;
330
        let missingConsts: string[] = [];
5✔
331
        let lines = fileContents.split(/\r?\n/g);
5✔
332

333
        let newLine: string;
334
        //loop through the lines until we find the bs_const line if it exists
335
        for (const line of lines) {
5✔
336
            if (line.toLowerCase().startsWith('bs_const')) {
53✔
337
                bsConstLine = line;
5✔
338
                newLine = line;
5✔
339
                break;
5✔
340
            }
341
        }
342

343
        if (bsConstLine) {
5!
344
            // update the consts in the manifest and check for missing consts
345
            missingConsts = Object.keys(consts).reduce((results, key) => {
5✔
346
                let match = new RegExp('(' + key + '\\s*=\\s*[true|false]+[^\\S\\r\\n]*\)', 'i').exec(bsConstLine);
7✔
347
                if (match) {
7!
348
                    newLine = newLine.replace(match[1], `${key}=${consts[key].toString()}`);
7✔
349
                } else {
350
                    results.push(key);
×
351
                }
352

353
                return results;
7✔
354
            }, []);
355

356
            // check for consts that where not in the manifest
357
            if (missingConsts.length > 0) {
5!
358
                throw new Error(`The following bs_const keys were not defined in the channel's manifest:\n\n${missingConsts.join(',\n')}`);
×
359
            } else {
360
                // update the manifest contents
361
                return fileContents.replace(bsConstLine, newLine);
5✔
362
            }
363
        } else {
364
            throw new Error('bs_const was defined in the launch.json but not in the channel\'s manifest');
×
365
        }
366
    }
367

368
    public static RALE_TRACKER_TASK_CODE = `if true = CreateObject("roAppInfo").IsDev() then m.vscode_rale_tracker_task = createObject("roSGNode", "TrackerTask") ' Roku Advanced Layout Editor Support`;
1✔
369
    public static RALE_TRACKER_ENTRY = 'vscode_rale_tracker_entry';
1✔
370
    /**
371
     * Search the project files for the comment "' vscode_rale_tracker_entry" and replace it with the code needed to start the TrackerTask.
372
     */
373
    public async copyAndTransformRaleTrackerTask() {
374
        // inject the tracker task into the staging files if we have everything we need
375
        if (!this.injectRaleTrackerTask || !this.raleTrackerTaskFileLocation) {
21✔
376
            return;
8✔
377
        }
378
        try {
13✔
379
            await fsExtra.copy(this.raleTrackerTaskFileLocation, s`${this.stagingFolderPath}/components/TrackerTask.xml`);
13✔
380
            this.logger.log('Tracker task successfully injected');
13✔
381
            // Search for the tracker task entry injection point
382
            const trackerReplacementResult = await replaceInFile({
13✔
383
                files: `${this.stagingFolderPath}/**/*.+(xml|brs)`,
384
                from: new RegExp(`^.*'\\s*${Project.RALE_TRACKER_ENTRY}.*$`, 'mig'),
385
                to: (match: string) => {
386
                    // Strip off the comment
387
                    let startOfLine = match.substring(0, match.indexOf(`'`));
12✔
388
                    if (/[\S]/.exec(startOfLine)) {
12✔
389
                        // There was some form of code before the tracker entry
390
                        // append and use single line syntax
391
                        startOfLine += ': ';
6✔
392
                    }
393
                    return `${startOfLine}${Project.RALE_TRACKER_TASK_CODE}`;
12✔
394
                }
395
            });
396
            const injectedFiles = trackerReplacementResult
13✔
397
                .filter(result => result.hasChanged)
26✔
398
                .map(result => result.file);
12✔
399

400
            if (injectedFiles.length === 0) {
13✔
401
                console.error(`WARNING: Unable to find an entry point for Tracker Task.\nPlease make sure that you have the following comment in your BrightScript project: "\' ${Project.RALE_TRACKER_ENTRY}"`);
1✔
402
            }
403
        } catch (err) {
404
            console.error(err);
×
405
        }
406
    }
407

408
    public static RDB_ODC_NODE_CODE = `if true = CreateObject("roAppInfo").IsDev() then m.vscode_rdb_odc_node = createObject("roSGNode", "RTA_OnDeviceComponent") ' RDB OnDeviceComponent`;
1✔
409
    public static RDB_ODC_ENTRY = 'vscode_rdb_on_device_component_entry';
1✔
410
    /**
411
     * Search the project files for the RTA_ODC_ENTRY comment and replace it with the code needed to start RTA_OnDeviceComponent which is used by RDB.
412
     */
413
    public async copyAndTransformRDB() {
414
        // inject the on device component into the staging files if we have everything we need
415
        if (!this.injectRdbOnDeviceComponent || !this.rdbFilesBasePath) {
21✔
416
            return;
7✔
417
        }
418
        try {
14✔
419
            let files = await globAsync(`${this.rdbFilesBasePath}/**/*`, {
14✔
420
                cwd: './',
421
                absolute: false,
422
                follow: true
423
            });
424
            for (let filePathAbsolute of files) {
14✔
425
                const promises = [];
52✔
426
                //only include files (i.e. skip directories)
427
                if (await util.isFile(filePathAbsolute)) {
52✔
428
                    const relativePath = s`${filePathAbsolute}`.replace(s`${this.rdbFilesBasePath}`, '');
26✔
429
                    const destinationPath = s`${this.stagingFolderPath}/${relativePath}`;
26✔
430
                    promises.push(fsExtra.copy(filePathAbsolute, destinationPath));
26✔
431
                }
432
                await Promise.all(promises);
52✔
433
                this.logger.log('RDB OnDeviceComponent successfully injected');
52✔
434
            }
435

436
            // Search for the tracker task entry injection point
437
            const replacementResult = await replaceInFile({
14✔
438
                files: `${this.stagingFolderPath}/**/*.+(xml|brs)`,
439
                from: new RegExp(`^.*'\\s*${Project.RDB_ODC_ENTRY}.*$`, 'mig'),
440
                to: (match: string) => {
441
                    // Strip off the comment
442
                    let startOfLine = match.substring(0, match.indexOf(`'`));
12✔
443
                    if (/[\S]/.exec(startOfLine)) {
12✔
444
                        // There was some form of code before the tracker entry
445
                        // append and use single line syntax
446
                        startOfLine += ': ';
6✔
447
                    }
448
                    return `${startOfLine}${Project.RDB_ODC_NODE_CODE}`;
12✔
449
                }
450
            });
451
            const injectedFiles = replacementResult
13✔
452
                .filter(result => result.hasChanged)
39✔
453
                .map(result => result.file);
12✔
454

455
            if (injectedFiles.length === 0) {
13✔
456
                console.error(`WARNING: Unable to find an entry point for RDB.\nPlease make sure that you have the following comment in your BrightScript project: "\' ${Project.RDB_ODC_ENTRY}"`);
1✔
457
            }
458
        } catch (err) {
459
            console.error(err);
1✔
460
        }
461
    }
462

463
    /**
464
     *
465
     * @param stagingPath
466
     */
467
    public async zipPackage(params: { retainStagingFolder: true }) {
468
        await rokuDeploy.zipPackage({
1✔
469
            ...this,
470
            ...params
471
        });
472
    }
473

474
    /**
475
     * Get the file paths from roku-deploy, and ensure the dest paths are absolute
476
     * (`dest` paths are relative in later versions of roku-deploy)
477
     */
478
    protected async getFileMappings() {
479
        let fileMappings = await rokuDeploy.getFilePaths(this.files, this.rootDir);
9✔
480
        for (let mapping of fileMappings) {
9✔
481
            //if the dest path is relative, make it absolute (relative to the staging dir)
482
            mapping.dest = path.resolve(this.stagingFolderPath, mapping.dest);
11✔
483
            //standardize the paths once here, and don't need to do it again anywhere else in this project
484
            mapping.src = fileUtils.standardizePath(mapping.src);
11✔
485
            mapping.dest = fileUtils.standardizePath(mapping.dest);
11✔
486
        }
487
        return fileMappings;
9✔
488
    }
489
}
490

491
export interface ComponentLibraryConstructorParams extends AddProjectParams {
492
    outFile: string;
493
    libraryIndex: number;
494
}
495

496
export class ComponentLibraryProject extends Project {
1✔
497
    constructor(params: ComponentLibraryConstructorParams) {
498
        super(params);
74✔
499
        this.outFile = params.outFile;
74✔
500
        this.libraryIndex = params.libraryIndex;
74✔
501
    }
502
    public outFile: string;
503
    public libraryIndex: number;
504
    /**
505
     * The name of the component library that this project represents. This is loaded during `this.computeOutFileName`
506
     */
507
    public name: string;
508

509
    /**
510
     * Takes a component Library and checks the outFile for replaceable values pulled from the libraries manifest
511
     * @param manifestPath the path to the manifest file to check
512
     */
513
    private async computeOutFileName(manifestPath: string) {
514
        let regexp = /\$\{([\w\d_]*)\}/;
7✔
515
        let renamingMatch: RegExpExecArray;
516
        let manifestValues = await util.convertManifestToObject(manifestPath);
7✔
517
        if (!manifestValues) {
7✔
518
            throw new Error(`Cannot find manifest file at "${manifestPath}"\n\nCould not complete automatic component library naming.`);
1✔
519
        }
520

521
        //load the component libary name from the manifest
522
        this.name = manifestValues.sg_component_libs_provided;
6✔
523

524
        // search the outFile for replaceable values such as ${title}
525
        while ((renamingMatch = regexp.exec(this.outFile))) {
6✔
526

527
            // replace the replaceable key with the manifest value
528
            let manifestVariableName = renamingMatch[1];
5✔
529
            let manifestVariableValue = manifestValues[manifestVariableName];
5✔
530
            if (manifestVariableValue) {
5!
531
                this.outFile = this.outFile.replace(renamingMatch[0], manifestVariableValue);
5✔
532
            } else {
533
                throw new Error(`Cannot find manifest value:\n"${manifestVariableName}"\n\nCould not complete automatic component library naming.`);
×
534
            }
535
        }
536
    }
537

538
    public async stage() {
539
        /*
540
         Compute the file mappings now (i.e. don't let the parent class compute them).
541
         This must be done BEFORE finding the manifest file location.
542
         */
543
        this.fileMappings = await this.getFileMappings();
6✔
544

545
        let expectedManifestDestPath = fileUtils.standardizePath(`${this.stagingFolderPath}/manifest`).toLowerCase();
6✔
546
        //find the file entry with the `dest` value of `${stagingFolderPath}/manifest` (case insensitive)
547
        let manifestFileEntry = this.fileMappings.find(x => x.dest.toLowerCase() === expectedManifestDestPath);
6✔
548
        if (manifestFileEntry) {
6!
549
            //read the manifest from `src` since nothing has been copied to staging yet
550
            await this.computeOutFileName(manifestFileEntry.src);
6✔
551
        } else {
552
            throw new Error(`Could not find manifest path for component library at '${this.rootDir}'`);
×
553
        }
554
        let fileNameWithoutExtension = path.basename(this.outFile, path.extname(this.outFile));
6✔
555

556
        let defaultStagingFolderPath = this.stagingFolderPath;
6✔
557

558
        //compute the staging folder path.
559
        this.stagingFolderPath = s`${this.outDir}/${fileNameWithoutExtension}`;
6✔
560

561
        /*
562
          The fileMappings were created using the default stagingFolderPath (because we need the manifest path
563
          to compute the out file name and staging path), so we need to replace the default stagingFolderPath
564
          with the actual stagingFolderPath.
565
         */
566
        for (let fileMapping of this.fileMappings) {
6✔
567
            fileMapping.dest = fileUtils.replaceCaseInsensitive(fileMapping.dest, defaultStagingFolderPath, this.stagingFolderPath);
7✔
568
        }
569

570
        return super.stage();
6✔
571
    }
572

573
    /**
574
     * The text used as a postfix for every brs file so we can accurately track the location of the files
575
     * back to their original component library whenever the debugger truncates the file path.
576
     */
577
    public get postfix() {
578
        return `${componentLibraryPostfix}${this.libraryIndex}`;
6✔
579
    }
580

581
    public async postfixFiles() {
582
        let pathDetails = {};
×
583
        await Promise.all(this.fileMappings.map(async (fileMapping) => {
×
584
            let relativePath = fileUtils.removeLeadingSlash(
×
585
                fileUtils.getRelativePath(this.stagingFolderPath, fileMapping.dest)
586
            );
587
            let postfixedPath = fileUtils.postfixFilePath(relativePath, this.postfix, ['.brs']);
×
588
            if (postfixedPath !== relativePath) {
×
589
                // Rename the brs files to include the postfix namespacing tag
590
                await fsExtra.move(fileMapping.dest, path.join(this.stagingFolderPath, postfixedPath));
×
591
                // Add to the map of original paths and the new paths
592
                pathDetails[postfixedPath] = relativePath;
×
593
            }
594
        }));
595

596
        // Update all the file name references in the library to the new file names
597
        await replaceInFile({
×
598
            files: [
599
                path.join(this.stagingFolderPath, '**/*.xml'),
600
                path.join(this.stagingFolderPath, '**/*.brs')
601
            ],
602
            from: /uri\s*=\s*"(.+)\.brs"/gi,
603
            to: (match) => {
604
                return match.replace('.brs', this.postfix + '.brs');
×
605
            }
606
        });
607
    }
608
}
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