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

rokucommunity / ropm / #874

23 Jan 2024 06:13PM UTC coverage: 89.291%. Remained the same
#874

push

GitHub
Merge 741ad53e2 into 803e66631

517 of 626 branches covered (0.0%)

Branch coverage included in aggregate %.

642 of 672 relevant lines covered (95.54%)

66.73 hits per line

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

100.0
/src/prefixer/RopmModule.ts
1
import { File } from './File';
1✔
2
import type { RopmPackageJson } from '../util';
3
import { util } from '../util';
1✔
4
import * as path from 'path';
1✔
5
import * as packlist from 'npm-packlist';
1✔
6
import * as rokuDeploy from 'roku-deploy';
1✔
7
import type { Dependency } from './ModuleManager';
8
import type { Program } from 'brighterscript';
9
import { ProgramBuilder } from 'brighterscript';
1✔
10
import { LogLevel } from 'brighterscript/dist/Logger';
1✔
11

12
export class RopmModule {
1✔
13
    constructor(
14
        public readonly hostRootDir: string,
135✔
15
        /**
16
         * The directory at the root of the module. This is the folder where the package.json resides
17
         */
18
        public readonly moduleDir: string
135✔
19
    ) {
20
        this.npmAliasName = util.getModuleName(this.moduleDir) as string;
135✔
21

22
        //compute the ropm name for this alias. This name has all invalid chars removed, and can be used as a brightscript variable/namespace
23
        this.ropmModuleName = util.getRopmNameFromModuleName(this.npmAliasName);
135✔
24
    }
25

26
    /**
27
     * A list of globs that will always be ignored during copy from node_modules to roku_modules
28
     */
29
    static readonly fileIgnorePatterns = [
1✔
30
        '!**/package.json',
31
        '!./README',
32
        '!./CHANGES',
33
        '!./CHANGELOG',
34
        '!./HISTORY',
35
        '!./LICENSE',
36
        '!./LICENCE',
37
        '!./NOTICE',
38
        '!**/.git',
39
        '!**/.svn',
40
        '!**/.hg',
41
        '!**/.lock-wscript',
42
        '!**/.*.swp',
43
        '!**/.DS_Store',
44
        '!**/npm-debug.log',
45
        '!**/.npmrc',
46
        '!**/node_modules',
47
        '!**/config.gypi',
48
        '!**/*.orig',
49
        '!**/package-lock.json',
50
        //package authors should exclude `roku_modules` during publishing, but things might slip through the cracks, so exclude those during ropm install
51
        '!**/roku_modules/**/*'
52
    ];
53

54
    public files = [] as File[];
135✔
55

56
    /**
57
     * The name of this module. Users can rename modules on install-time, so this is the folder we must use
58
     */
59
    public npmAliasName: string;
60

61
    /**
62
     * The name of the module directly from the module's package.json. This is used to help resolve dependencies between packages
63
     * even if an alias is used
64
     */
65
    public npmModuleName!: string;
66

67
    /**
68
     * The version of this current module
69
     */
70
    public version!: string;
71

72
    /**
73
     * The ropm name of this module. ROPM module names are sanitized npm names.
74
     */
75
    public ropmModuleName: string;
76

77
    /**
78
     * The path where this module's package code resides.
79
     */
80
    public packageRootDir!: string;
81

82
    /**
83
     * A map from the original file location to its new destination.
84
     * This is set during the copy process.
85
     */
86
    public fileMaps!: Array<{ src: string; dest: string }>;
87

88
    /**
89
     * A map from the prefixes used when this module was published, to the prefix that should be used
90
     * when this module is installed in the overall project.
91
     * This depends on the module properly referencing every dependency.
92
     */
93
    public prefixMap = {} as Record<string, string>;
135✔
94

95
    /**
96
     * The contents of the package.json
97
     */
98
    public packageJson!: RopmPackageJson;
99

100
    /**
101
     * The dominant version of the dependency. This will be the major version in most cases, will be the full version string for pre-release versions
102
     */
103
    public dominantVersion!: string;
104

105
    public isValid = true;
135✔
106

107
    public async init() {
108
        //skip modules we can't derive a name from
109
        if (!this.npmAliasName) {
172✔
110
            console.error(`ropm: cannot compute npm package name for "${this.moduleDir}"`);
1✔
111
            this.isValid = false;
1✔
112
            return;
1✔
113
        }
114

115
        this.packageJson = await util.getPackageJson(this.moduleDir);
171✔
116
        this.version = this.packageJson.version;
171✔
117
        this.dominantVersion = util.getDominantVersion(this.packageJson.version);
171✔
118

119
        if (!this.packageJson.name) {
171✔
120
            console.error(`ropm: missing "name" property from "${path.join(this.moduleDir, 'package.json')}"`);
1✔
121
            this.isValid = false;
1✔
122
            return;
1✔
123
        }
124

125
        this.npmModuleName = this.packageJson.name;
170✔
126

127
        // every ropm module MUST have the `ropm` keyword. If not, then this is not a ropm module
128
        if ((this.packageJson.keywords ?? []).includes('ropm') === false) {
170✔
129
            console.error(`ropm: skipping prod dependency "${this.moduleDir}" because it does not have the "ropm" keyword`);
3✔
130
            this.isValid = false;
3✔
131
            return;
3✔
132
        }
133

134
        //disallow using `noprefix` within dependencies
135
        if (this.packageJson.ropm?.noprefix) {
167✔
136
            console.error(`ropm: using "ropm.noprefix" in a ropm module is forbidden: "${path.join(this.moduleDir, 'package.json')}`);
1✔
137
            this.isValid = false;
1✔
138
            return;
1✔
139
        }
140

141
        //use the rootDir from packageJson, or default to the current module path
142
        this.packageRootDir = this.packageJson.ropm?.packageRootDir ? path.resolve(this.moduleDir, this.packageJson.ropm.packageRootDir) : this.moduleDir;
166✔
143
    }
144

145
    public async copyFiles() {
146
        const packageLogText = `${this.npmAliasName}${this.npmAliasName !== this.npmModuleName ? `(${this.npmModuleName})` : ''}`;
101✔
147

148
        console.log(`ropm: copying ${packageLogText}@${this.version} as ${this.ropmModuleName}`);
101✔
149
        //use the npm-packlist project to get the list of all files for the entire package...use this as the whitelist
150
        let allFiles = await packlist({
101✔
151
            path: this.packageRootDir
152
        });
153

154
        //standardize each path
155
        allFiles = allFiles.map((f) => rokuDeploy.util.standardizePath(f));
209✔
156

157
        //get the list of all file paths within the rootDir
158
        let rootDirFiles = await util.globAll([
101✔
159
            '**/*',
160
            ...RopmModule.fileIgnorePatterns
161
        ], {
162
            cwd: this.packageRootDir,
163
            //follow symlinks
164
            follow: true,
165
            dot: true,
166
            //skip matching folders (we'll handle file copying ourselves)
167
            nodir: true
168
        });
169

170
        //standardize each path
171
        rootDirFiles = rootDirFiles.map((f) => rokuDeploy.util.standardizePath(f));
115✔
172

173
        const files = rootDirFiles
101✔
174

175
            //only keep files that are both in the packlist AND the rootDir list
176
            .filter((rootDirFile) => {
177
                return allFiles.includes(
115✔
178
                    rootDirFile
179
                );
180
            })
181

182
            .filter((rootDirFile) => {
183
                //filter top-level files (all files should be contained within a subfolder)
184
                const fileIsLocatedInAFolder = !!/\\|\//.exec(rootDirFile);
109✔
185
                return fileIsLocatedInAFolder;
109✔
186
            });
187

188
        //create a map of every source file and where it should be copied to
189
        this.fileMaps = files.map(filePath => {
101✔
190
            const filePathParts = filePath.split(/\/|\\/);
105✔
191
            const topLevelDir = filePathParts.splice(0, 1)[0];
105✔
192
            const targetPath = path.join(this.hostRootDir, topLevelDir, 'roku_modules', this.ropmModuleName, ...filePathParts);
105✔
193
            return {
105✔
194
                src: path.resolve(this.packageRootDir, filePath),
195
                dest: targetPath
196
            };
197
        });
198

199
        //copy the files for this module to their destinations in the host root dir
200
        await util.copyFiles(this.fileMaps);
101✔
201
    }
202

203
    private program!: Program;
204

205
    /**
206
     * @param noprefix a list of npm aliases of modules that should NOT be prefixed
207
     */
208
    public async transform(noprefixRopmAliases: string[]) {
209
        const builder = new ProgramBuilder();
101✔
210

211
        //disable the Program.validate function to improve performance (we don't care about the validity of the code...that should be handled by the package publisher)
212
        util.mockProgramValidate();
101✔
213
        await builder.run({
101✔
214
            //specify an optional bogus bsconfig which prevents loading any bsconfig found in cwd
215
            project: '?___not-real-bsconfig.json",',
216
            copyToStaging: false,
217
            createPackage: false,
218
            rootDir: this.packageRootDir,
219
            cwd: this.packageRootDir,
220
            //hide all diagnostics, the package author should be responsible for ensuring their package is valid
221
            diagnosticFilters: ['**/*'],
222
            //hide log statements
223
            logLevel: LogLevel.error,
224
            //include all files except node_modules and roku_modules (publishers SHOULD be excluding those, but might not)
225
            files: [
226
                '**/*',
227
                '!**/roku_modules/**/*',
228
                '!**/node_modules/**/*'
229
            ]
230
        });
231
        this.program = builder.program;
101✔
232

233
        //load all files
234
        for (const obj of this.fileMaps) {
101✔
235
            //only load source code files
236
            if (['.xml', '.brs', '.bs'].includes(path.extname(obj.dest).toLowerCase())) {
105✔
237
                this.files.push(
104✔
238
                    new File(obj.src, obj.dest, this.packageRootDir, this.packageJson.ropm)
239
                );
240
            }
241
        }
242

243
        //let all files discover all functions/components
244
        for (const file of this.files) {
101✔
245
            file.discover(this.program);
104✔
246
        }
247

248

249
        //create the edits for every file
250
        this.createEdits(noprefixRopmAliases);
101✔
251

252
        //apply all of the edits
253
        for (const file of this.files) {
101✔
254
            file.applyEdits();
104✔
255
        }
256

257
        //write the files back to disk with their changes applied
258
        await Promise.all(
101✔
259
            this.files.map((file) => file.write())
104✔
260
        );
261
    }
262

263
    private readonly nonPrefixedFunctionMap = {
135✔
264
        'runuserinterface': true,
265
        'main': true,
266
        'runscreensaver': true,
267
        'init': true,
268
        'onkeyevent': true
269
    };
270

271
    /**
272
     * Create the prefix map for this module
273
     * @param programDependencies - the full list of resolved dependencies from the program. This is created by ModuleManager based on all modules in the program.
274
     */
275
    public async createPrefixMap(programDependencies: Dependency[]) {
276
        //reassign own module name based on program dependencies
277
        const ownDependency = programDependencies.find(
109✔
278
            x => x.npmModuleName === this.npmModuleName && x.dominantVersion === this.dominantVersion
133✔
279
        );
280
        if (!ownDependency) {
109✔
281
            throw new Error(`Cannot find ${this.npmModuleName}@${this.dominantVersion} in programDependencies`);
1✔
282
        }
283
        //rename the ropm module name based on the program dependency. (this could be postfixed with a _v1, _v2, etc. if there is dependency resolution in play)
284
        this.ropmModuleName = ownDependency.ropmModuleName;
108✔
285

286
        //compute all of the names of the dependencies within this module, and what prefixes we currently used for them.
287
        const deps = await util.getModuleDependencies(this.moduleDir);
108✔
288
        this.prefixMap = {};
108✔
289
        for (const dep of deps) {
108✔
290
            const depDominantVersion = util.getDominantVersion(dep.version);
17✔
291
            const programDependency = programDependencies.find(
17✔
292
                (x) => x.npmModuleName === dep.npmModuleName && x.dominantVersion === depDominantVersion
34✔
293
            );
294

295
            if (programDependency) {
17✔
296
                this.prefixMap[dep.ropmModuleName] = programDependency.ropmModuleName;
15✔
297
            } else {
298
                const dependencyText = dep.npmAlias === dep.npmModuleName ? dep.npmAlias : `${dep.npmAlias}(${dep.npmModuleName})`;
2✔
299
                throw new Error(`Cannot find suitable program dependency for ${dependencyText}@${dep.version}`);
2✔
300
            }
301
        }
302
    }
303

304
    private getInterfaceFunctions() {
305
        const names = {} as Record<string, boolean>;
101✔
306
        for (const file of this.files) {
101✔
307
            for (const func of file.componentInterfaceFunctions) {
104✔
308
                names[func.name.toLowerCase()] = true;
5✔
309
            }
310
        }
311
        return names;
101✔
312
    }
313

314
    private createEdits(noprefixRopmAliases: string[]) {
315
        const applyOwnPrefix = !noprefixRopmAliases.includes(this.ropmModuleName);
101✔
316

317
        const prefix = this.ropmModuleName + '_';
101✔
318
        const brighterscriptPrefix = this.ropmModuleName.replace(/_/g, '.');
101✔
319
        const ownFunctionMap = this.getDistinctFunctionDeclarationMap();
101✔
320
        const ownComponentNames = this.getDistinctComponentDeclarationNames();
101✔
321
        const prefixMapKeys = Object.keys(this.prefixMap);
101✔
322
        const prefixMapKeysLower = prefixMapKeys.map(x => x.toLowerCase());
101✔
323

324
        /**
325
         * Get the alias for a namespace. Only returns if it exists and is different than what is given.
326
         */
327
        const getAlias = (namespace?: string) => {
101✔
328
            if (namespace) {
13✔
329
                const lowerNamespaceName = namespace.toLowerCase();
10✔
330
                const idx = prefixMapKeysLower.indexOf(lowerNamespaceName);
10✔
331
                const prefix = this.prefixMap[prefixMapKeys[idx]];
10✔
332
                if (prefix && prefix.toLowerCase() !== lowerNamespaceName) {
10✔
333
                    return prefix;
2✔
334
                }
335
            }
336
        };
337

338
        const nonPrefixedFunctionMap = {
101✔
339
            ...this.nonPrefixedFunctionMap,
340
            ...this.getInterfaceFunctions()
341
        };
342
        const ropmPrefixSourceLiteralValue = `"${prefix}"`;
101✔
343

344
        for (const file of this.files) {
101✔
345

346
            //rewrite file references
347
            for (const fileReference of file.fileReferences) {
104✔
348
                this.createFileReferenceEdit(file, fileReference);
19✔
349
            }
350

351
            //apply prefix-related transforms
352
            if (applyOwnPrefix) {
104✔
353

354
                // replace `m.top.functionName = "<anything>"` assignments to support Tasks
355
                for (const ref of file.taskFunctionNameAssignments) {
95✔
356
                    file.addEdit(ref.offset, ref.offset, prefix);
3✔
357
                }
358

359
                //create an edit for each this-module-owned function
360
                for (const func of file.functionDefinitions) {
95✔
361
                    const lowerName = func.name.toLowerCase();
97✔
362

363
                    //skip edits for special functions
364
                    if (nonPrefixedFunctionMap[lowerName]) {
97✔
365
                        continue;
32✔
366
                    }
367

368
                    //handle typedef (.d.bs) files
369
                    if (file.isTypdefFile) {
65✔
370
                        //wrap un-namespaced functions with a namespace
371
                        if (!func.hasNamespace) {
14✔
372

373
                            file.addEdit(func.startOffset, func.startOffset, `namespace ${brighterscriptPrefix}\n`);
7✔
374
                            file.addEdit(func.endOffset, func.endOffset, `\nend namespace`);
7✔
375
                        }
376
                        continue;
14✔
377
                        //functions with leading underscores are treated specially
378
                    } else if (func.name.startsWith('_')) {
51✔
379
                        const leadingUnderscores = /^_+/.exec(func.name)![0];
3✔
380
                        file.addEdit(func.nameOffset + leadingUnderscores.length, func.nameOffset + leadingUnderscores.length, `${this.ropmModuleName}_`);
3✔
381
                    } else {
382
                        //is NOT a typedef file, and is not a nonPrefixed function, so prefix it
383
                        file.addEdit(func.nameOffset, func.nameOffset, prefix);
48✔
384
                    }
385
                }
386

387
                //wrap un-namespaced classes with prefix namespace
388
                for (const cls of file.classDeclarations) {
95✔
389
                    if (!cls.hasNamespace) {
16✔
390
                        file.addEdit(cls.startOffset, cls.startOffset, `namespace ${brighterscriptPrefix}\n`);
8✔
391
                        file.addEdit(cls.endOffset, cls.endOffset, `\nend namespace`);
8✔
392
                    }
393
                }
394

395
                //prefix d.bs class references
396
                for (const ref of file.classReferences) {
95✔
397
                    const baseNamespace = util.getBaseNamespace(ref.fullyQualifiedName);
13✔
398

399
                    const alias = getAlias(baseNamespace);
13✔
400
                    let fullyQualifiedName: string;
401
                    //if we have an alias, this is a class from another module.
402
                    if (alias) {
13✔
403
                        fullyQualifiedName = ref.fullyQualifiedName.replace(/^.*?\./, alias + '.');
2✔
404
                    } else {
405
                        //this is an internal-module class, so append our prefix to it
406
                        fullyQualifiedName = `${brighterscriptPrefix}.${ref.fullyQualifiedName}`;
11✔
407
                    }
408
                    file.addEdit(ref.offsetBegin, ref.offsetEnd, fullyQualifiedName);
13✔
409
                }
410

411
                //prefix d.bs namespaces
412
                for (const namespace of file.namespaces) {
95✔
413
                    file.addEdit(namespace.offset, namespace.offset, brighterscriptPrefix + '.');
7✔
414
                }
415

416
                //prefix all function calls to our own function names
417
                for (const call of file.functionReferences) {
95✔
418
                    const lowerName = call.name.toLowerCase();
33✔
419

420
                    //skip edits for special functions
421
                    if (nonPrefixedFunctionMap[lowerName]) {
33✔
422
                        continue;
1✔
423
                    }
424
                    //if this function is owned by our project, rename it
425
                    if (ownFunctionMap[lowerName]) {
32✔
426
                        if (lowerName.startsWith('_')) {
16✔
427
                            const leadingUnderscores = /^_+/.exec(lowerName)![0];
3✔
428
                            file.addEdit(call.offset + leadingUnderscores.length, call.offset + leadingUnderscores.length, `${this.ropmModuleName}_`);
3✔
429
                        } else {
430
                            file.addEdit(call.offset, call.offset, prefix);
13✔
431
                        }
432
                        continue;
16✔
433
                    }
434

435
                    //rename dependency function calls
436
                    const possiblePrefix = lowerName.split('_')[0];
16✔
437
                    const idx = prefixMapKeysLower.indexOf(possiblePrefix);
16✔
438
                    //if we have a prefix match, then convert the old prefix to the new prefix
439
                    if (idx > -1) {
16✔
440
                        const newPrefix = this.prefixMap[prefixMapKeys[idx]];
2✔
441
                        //begin position + the length of the original prefix + 1 for the underscore
442
                        const offsetEnd = call.offset + possiblePrefix.length + 1;
2✔
443
                        file.addEdit(call.offset, offsetEnd, newPrefix + '_');
2✔
444
                    }
445
                }
446

447
                //replace ROPM_PREFIX source literals and prefix identifiers with same name as function
448
                for (const identifier of file.identifiers) {
95✔
449
                    const lowerName = identifier.name.toLowerCase();
37✔
450

451
                    //replace ROPM_PREFIX source literal
452
                    if (lowerName === 'ropm_prefix') {
37✔
453
                        file.addEdit(identifier.offset, identifier.offset + identifier.name.length, ropmPrefixSourceLiteralValue);
1✔
454
                    }
455

456
                    //skip edits for special functions
457
                    if (nonPrefixedFunctionMap[lowerName]) {
37✔
458
                        continue;
2✔
459
                    }
460
                    //if this identifier has the same name as a function, then prefix the identifier
461
                    if (ownFunctionMap[lowerName]) {
35✔
462
                        const leadingEditText = file.isTypdefFile ? `${brighterscriptPrefix}.` : prefix;
2✔
463
                        file.addEdit(identifier.offset, identifier.offset, leadingEditText);
2✔
464
                    }
465
                }
466

467
                //rename all this-file-defined component definitions
468
                for (const comp of file.componentDeclarations) {
95✔
469
                    file.addEdit(comp.offset, comp.offset, prefix);
16✔
470
                }
471

472
                //rename all component usage
473
                for (const comp of file.componentReferences) {
95✔
474
                    //if this component is owned by our module, rename it
475
                    if (ownComponentNames.includes(comp.name.toLowerCase())) {
14✔
476
                        file.addEdit(comp.offset, comp.offset, prefix);
7✔
477

478
                        //rename dependency component usage
479
                    } else {
480
                        const possiblePrefix = comp.name.toLowerCase().split('_')[0];
7✔
481
                        const idx = prefixMapKeysLower.indexOf(possiblePrefix);
7✔
482
                        //if we have a prefix match, then convert the old prefix to the new prefix
483
                        if (idx > -1) {
7✔
484
                            const newPrefix = this.prefixMap[prefixMapKeys[idx]];
2✔
485
                            //begin position + the length of the original prefix + 1 for the underscore
486
                            const offsetEnd = comp.offset + possiblePrefix.length + 1;
2✔
487
                            file.addEdit(comp.offset, offsetEnd, newPrefix + '_');
2✔
488
                        }
489
                    }
490
                }
491
            }
492
        }
493
    }
494

495
    private createFileReferenceEdit(file: File, fileReference: { path: string; offset: number }) {
496
        let pkgPathAbsolute: string;
497
        if (fileReference.path.startsWith('pkg:')) {
19✔
498
            pkgPathAbsolute = fileReference.path;
8✔
499

500
            //relative path. resolve to absolute path
501
        } else {
502
            pkgPathAbsolute = `pkg:/` + path.posix.normalize(path.dirname(file.pkgPath) + '/' + fileReference.path);
11✔
503
        }
504

505
        const parts = pkgPathAbsolute.split('/');
19✔
506
        //discard the first part (pkg:)
507
        parts.splice(0, 1);
19✔
508
        const baseFolder = parts[0];
19✔
509
        //remove the base folder part
510
        parts.splice(0, 1);
19✔
511

512
        let newPath: string;
513

514
        //if the second folder is `roku_modules`
515
        if (parts[0] === 'roku_modules') {
19✔
516
            //remove the roku_modules bit
517
            parts.splice(0, 1);
3✔
518
            //this is a reference to a dependency's file
519
            const dependencyName = parts[0];
3✔
520
            //remove the dependency name bit
521
            parts.splice(0, 1);
3✔
522
            const newDependencyName = this.prefixMap[dependencyName.toLowerCase()];
3✔
523
            newPath = `pkg:/${baseFolder}/roku_modules/${newDependencyName}/${parts.join('/')}`;
3✔
524
        } else {
525
            //this is a reference to this module's own file
526
            newPath = `pkg:/${baseFolder}/roku_modules/${this.ropmModuleName}/${parts.join('/')}`;
16✔
527
        }
528
        file.addEdit(fileReference.offset, fileReference.offset + fileReference.path.length, newPath);
19✔
529
    }
530

531
    /**
532
     * Scan every file and compute the list of function declaration names.
533
     */
534
    public getDistinctFunctionDeclarationMap() {
535
        const result = {} as Record<string, boolean>;
101✔
536
        for (const file of this.files) {
101✔
537
            for (const func of file.functionDefinitions) {
104✔
538
                //skip the special function names
539
                if (this.nonPrefixedFunctionMap[func.name.toLowerCase()]) {
102✔
540
                    continue;
27✔
541
                }
542
                result[func.name.toLowerCase()] = true;
75✔
543
            }
544
        }
545
        return result;
101✔
546
    }
547

548
    /**
549
     * Get the distinct names of function calls
550
     */
551
    public getDistinctFunctionCallNames() {
552
        const result = {};
1✔
553
        for (const file of this.files) {
1✔
554
            for (const call of file.functionReferences) {
2✔
555
                result[call.name.toLowerCase()] = true;
2✔
556
            }
557
        }
558
        return Object.keys(result);
1✔
559
    }
560

561
    /**
562
     * Get the distinct names of component declarations
563
     */
564
    public getDistinctComponentDeclarationNames() {
565
        const result = {};
101✔
566
        for (const file of this.files) {
101✔
567
            for (const comp of file.componentDeclarations) {
104✔
568
                result[comp.name.toLowerCase()] = true;
20✔
569
            }
570
        }
571
        return Object.keys(result);
101✔
572
    }
573

574
    /**
575
     * Get the distinct names of components used
576
     */
577
    public getDistinctComponentReferenceNames() {
578
        const result = {};
1✔
579
        for (const file of this.files) {
1✔
580
            for (const comp of file.componentReferences) {
2✔
581
                result[comp.name.toLowerCase()] = true;
2✔
582
            }
583
        }
584
        return Object.keys(result);
1✔
585
    }
586
}
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