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

rokucommunity / ropm / #1351

23 Jan 2026 08:18PM UTC coverage: 89.047%. Remained the same
#1351

push

web-flow
Merge 025f2388b 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.08 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
                maxBuffer: 50 * 1024 * 1024 // 50MB buffer to handle large dependency trees
131
            }).toString();
132
        } catch (e: any) {
UNCOV
133
            stdout = (e as any).stdout.toString();
×
134

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

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

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

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

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

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