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

rokucommunity / ropm / #1346

23 Jan 2026 07:48PM UTC coverage: 89.047%. Remained the same
#1346

push

web-flow
Merge 881c7337b into f2472df9c

677 of 813 branches covered (83.27%)

Branch coverage included in aggregate %.

13 of 20 new or added lines in 1 file covered. (65.0%)

1 existing line in 1 file now uncovered.

697 of 730 relevant lines covered (95.48%)

68.05 hits per line

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

73.75
/src/commands/InstallCommand.ts
1
import type { CommandArgs, RopmPackageJson } from '../util';
2
import { util } from '../util';
1✔
3
import * as path from 'path';
1✔
4
import * as childProcess from 'child_process';
1✔
5
import * as fsExtra from 'fs-extra';
1✔
6
import { InitCommand } from './InitCommand';
1✔
7
import { CleanCommand } from './CleanCommand';
1✔
8
import { ModuleManager } from '../prefixer/ModuleManager';
1✔
9

10
export class InstallCommand {
1✔
11
    constructor(
12
        public args: InstallCommandArgs
26✔
13
    ) {
14

15
    }
16

17
    private hostPackageJson?: RopmPackageJson;
18

19
    public logger = util.createLogger();
26✔
20

21
    private moduleManager = new ModuleManager({ logger: this.logger });
26✔
22

23

24
    private get hostRootDir() {
25
        const packageJsonRootDir = this.args.rootDir ?? this.hostPackageJson?.ropm?.rootDir;
21!
26
        if (packageJsonRootDir) {
21✔
27
            return path.resolve(this.cwd, packageJsonRootDir);
4✔
28
        } else {
29
            return this.cwd;
17✔
30
        }
31
    }
32

33
    private get cwd() {
34
        if (this.args?.cwd) {
180!
35
            return path.resolve(process.cwd(), this.args?.cwd);
180!
36
        } else {
37
            return process.cwd();
×
38
        }
39
    }
40

41
    public async run(runNpmInstall = true): Promise<void> {
21✔
42
        await this.loadHostPackageJson();
21✔
43
        this.updateLogLevel();
21✔
44
        await this.deleteAllRokuModulesFolders();
21✔
45
        if (runNpmInstall) {
21!
46
            await this.npmInstall();
21✔
47
        }
48
        await this.processModules();
21✔
49
    }
50

51
    /**
52
     * Deletes every roku_modules folder found in the hostRootDir
53
     */
54
    private async deleteAllRokuModulesFolders() {
55
        const cleanCommand = new CleanCommand({
21✔
56
            cwd: this.cwd,
57
            logLevel: this.args.logLevel
58
        });
59
        await cleanCommand.run();
21✔
60
    }
61

62
    /**
63
     * A "host" is the project we are currently operating upon. This method
64
     * finds the package.json file for the current host
65
     */
66
    private async loadHostPackageJson() {
67
        //if the host doesn't currently have a package.json
68
        if (await fsExtra.pathExists(path.resolve(this.cwd, 'package.json')) === false) {
21!
69
            this.logger.log('Creating package.json');
×
70
            //init package.json for the host
71
            await new InitCommand({ cwd: this.cwd, force: true, promptForRootDir: true, logLevel: this.args.logLevel }).run();
×
72
        }
73
        this.hostPackageJson = await util.getPackageJson(this.cwd);
21✔
74
    }
75

76
    private updateLogLevel() {
77
        //set the logLevel provided by the RopmOptions
78
        this.logger.logLevel = this.args.logLevel ?? this.hostPackageJson?.ropm?.logLevel ?? 'log';
21!
79
    }
80

81
    private async npmInstall() {
82
        if (await fsExtra.pathExists(this.cwd) === false) {
21!
83
            throw new Error(`"${this.cwd}" does not exist`);
×
84
        }
85
        await util.spawnNpmAsync([
21✔
86
            'i',
87
            ...(this.args.packages ?? [])
63✔
88
        ], {
89
            cwd: this.cwd
90
        });
91
    }
92

93
    /**
94
     * Copy all modules to roku_modules
95
     */
96
    private async processModules() {
97
        const modulePaths = this.getProdDependencies();
21✔
98

99
        //remove the host module from the list (it should always be the first entry)
100
        const hostModulePath = modulePaths.splice(0, 1)[0];
21✔
101
        this.moduleManager.hostDependencies = await util.getModuleDependencies(hostModulePath, this.logger);
21✔
102

103
        this.moduleManager.hostRootDir = this.hostRootDir;
21✔
104
        this.moduleManager.noprefixNpmAliases = this.hostPackageJson?.ropm?.noprefix ?? [];
21!
105

106
        //copy all of them at once, wait for them all to complete
107
        for (const modulePath of modulePaths) {
21✔
108
            this.moduleManager.addModule(modulePath);
21✔
109
        }
110

111
        await this.moduleManager.process();
21✔
112
    }
113

114
    /**
115
     * Get the list of prod dependencies from npm.
116
     * This is run sync because it should run as fast as possible
117
     * and won't be run in ~parallel.
118
     */
119
    public getProdDependencies() {
120
        if (fsExtra.pathExistsSync(this.cwd) === false) {
27!
121
            throw new Error(`"${this.cwd}" does not exist`);
×
122
        }
123
        let stdout: string;
124
        try {
27✔
125
            const npmLs = `npm ls --json --long --omit=dev --omit=optional --depth=Infinity`;
27✔
126
            this.logger.debug(`executing command: ${npmLs}`);
27✔
127

128
            stdout = childProcess.execSync(npmLs, {
27✔
129
                cwd: this.cwd
130
            }).toString();
131
        } catch (e: any) {
132
            stdout = (e as any).stdout.toString();
×
133

134
            // do not throw error, just log a warning
135
            // there are a lot of edge cases where npm ls errors don't pose any actual roadblock for ropm packages
136

UNCOV
137
            this.logger.warn([
×
138
                'Encountered an error while retrieving the prod dependencies from npm-ls. Attempting to proceed anyways. You can review the error below:\n',
139
                (e as any).message
140
            ].join('\n'));
141
        }
142

143
        const dependencyJson = JSON.parse(stdout);
27✔
144
        const thisPackage = this.findDependencyByName(dependencyJson, this.hostPackageJson?.name);
27✔
145
        const dependencies = this.flattenPackage(thisPackage);
27✔
146
        return dependencies;
27✔
147
    }
148

149
    /**
150
     * Flatten dependencies from `npm ls --json --long` to match the parseable output
151
     * @param packageJson the result from `npm ls --json --long`
152
     * @returns list of dependency paths
153
     */
154
    private flattenPackage(packageJson: any): string[] {
155
        const dependencies: string[] = [];
56✔
156
        if (packageJson) {
56!
157
            dependencies.push(packageJson.path);
56✔
158
            for (const dep of Object.values(packageJson.dependencies ?? {})) {
56!
159
                dependencies.push(...this.flattenPackage(dep));
29✔
160
            }
161
        }
162
        return dependencies;
56✔
163
    }
164

165
    /**
166
     * Finds the current package in the dependency tree
167
     *
168
     * Important for workspace projects, where the root project
169
     * is included in the dependency tree and needs to be removed
170
     * @param packageJson root depenendency json
171
     * @param name cwd project name
172
     * @returns package entry in dependency json
173
     */
174
    private findDependencyByName(packageJson: any, name: string | undefined) {
175
        if (packageJson.name === name || !name) {
27!
176
            return packageJson;
27✔
177
        }
NEW
178
        let foundPackage = Object.values(packageJson.dependencies ?? {}).find((d: any) => d.name === name);
×
NEW
179
        if (!foundPackage) {
×
NEW
180
            for (const key in (packageJson.dependencies ?? {})) {
×
NEW
181
                foundPackage = this.findDependencyByName(packageJson.dependencies[key], name);
×
NEW
182
                if (foundPackage) {
×
NEW
183
                    return foundPackage;
×
184
                }
185
            }
186
        }
NEW
187
        return foundPackage;
×
188
    }
189
}
190

191
export interface InstallCommandArgs extends CommandArgs {
192
    /**
193
     * The list of packages that should be installed
194
     */
195
    packages?: string[];
196
    /**
197
     * Dependencies installation location.
198
     * By default the setting from package.json is imported out-of-the-box, but if rootDir is passed here,
199
     * it will override the value from package.json.
200
     */
201
    rootDir?: string;
202
}
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