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

rokucommunity / ropm / #1301

20 Aug 2025 11:30AM UTC coverage: 89.803% (-0.5%) from 90.329%
#1301

push

web-flow
Merge aa5939274 into 8145979b9

679 of 810 branches covered (83.83%)

Branch coverage included in aggregate %.

6 of 7 new or added lines in 1 file covered. (85.71%)

686 of 710 relevant lines covered (96.62%)

68.28 hits per line

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

80.14
/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
25✔
13
    ) {
14

15
    }
16

17
    private hostPackageJson?: RopmPackageJson;
18

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

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

23

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

33
    private get cwd() {
34
        if (this.args?.cwd) {
177!
35
            return path.resolve(process.cwd(), this.args?.cwd);
177!
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];
20✔
101
        this.moduleManager.hostDependencies = await util.getModuleDependencies(hostModulePath);
20✔
102

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

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

111
        await this.moduleManager.process();
20✔
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) {
26!
121
            throw new Error(`"${this.cwd}" does not exist`);
×
122
        }
123
        let stdout: string;
124
        try {
26✔
125
            let omitArgs = `--omit=${this.args?.omit}`;
26!
126

127
            if (Array.isArray(this.args?.omit)) {
26!
NEW
128
                omitArgs = this.args?.omit.map((omitArg) => `--omit=${omitArg}`).join(' ');
×
129
            }
130

131
            const npmLs = `npm ls --parseable ${omitArgs} --depth=${this.args?.depth}`;
26!
132
            console.log(`executing command: ${npmLs}`);
26✔
133

134
            stdout = childProcess.execSync(npmLs, {
26✔
135
                cwd: this.cwd
136
            }).toString();
137
        } catch (e) {
138
            stdout = (e as any).stdout.toString();
1✔
139
            const stderr: string = (e as any).stderr.toString();
1✔
140
            //sometimes the unit tests absorb stderr...so as long as we have stdout, assume it's valid (and ignore the stderr)
141
            if (stderr.includes('npm ERR! extraneous:')) {
1!
142
                //ignore errors
143
            } else {
144
                throw new Error([
1✔
145
                    'Failed to compute prod dependencies.',
146
                    'If this error is coming from an optional dependency, consider supply the args --omit=dev --omit=optional.',
147
                    'If this error is coming from a transitive dependency, consider supply the args --depth=0.',
148
                    (e as any).message
149
                ].join('\n'));
150
            }
151
        }
152

153
        return stdout.trim().split(/\r?\n/);
25✔
154
    }
155
}
156

157
export interface InstallCommandArgs extends CommandArgs {
158
    /**
159
     * The list of packages that should be installed
160
     */
161
    packages?: string[];
162
    /**
163
     * Dependencies installation location.
164
     * By default the setting from package.json is imported out-of-the-box, but if rootDir is passed here,
165
     * it will override the value from package.json.
166
     */
167
    rootDir?: string;
168
    omit?: string;
169
    depth?: number;
170
}
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