• 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

73.43
/src/commands/InstallCommand.ts
1
import type { 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
27✔
13
    ) {
14

15
    }
16

17
    private hostPackageJson?: RopmPackageJson;
18

19
    private moduleManager = new ModuleManager();
27✔
20

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

30
    private get cwd() {
31
        if (this.args?.cwd) {
187!
32
            return path.resolve(process.cwd(), this.args?.cwd);
187!
33
        } else {
34
            return process.cwd();
×
35
        }
36
    }
37

38
    public async run(runNpmInstall = true): Promise<void> {
22✔
39
        await this.loadHostPackageJson();
22✔
40
        await this.deleteAllRokuModulesFolders();
22✔
41
        if (runNpmInstall) {
22!
42
            await this.npmInstall();
22✔
43
        }
44
        await this.processModules();
22✔
45
    }
46

47
    /**
48
     * Deletes every roku_modules folder found in the hostRootDir
49
     */
50
    private async deleteAllRokuModulesFolders() {
51
        const cleanCommand = new CleanCommand({
22✔
52
            cwd: this.cwd
53
        });
54
        await cleanCommand.run();
22✔
55
    }
56

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

71
    private async npmInstall() {
72
        if (await fsExtra.pathExists(this.cwd) === false) {
22!
73
            throw new Error(`"${this.cwd}" does not exist`);
×
74
        }
75
        await util.spawnNpmAsync([
22✔
76
            'i',
77
            ...(this.args.packages ?? [])
66✔
78
        ], {
79
            cwd: this.cwd
80
        });
81
    }
82

83
    /**
84
     * Copy all modules to roku_modules
85
     */
86
    private async processModules() {
87
        const modulePaths = this.getProdDependencies();
22✔
88

89
        //remove the host module from the list (it should always be the first entry)
90
        const hostModulePath = modulePaths.splice(0, 1)[0];
21✔
91
        this.moduleManager.hostDependencies = await util.getModuleDependencies(hostModulePath);
21✔
92

93
        this.moduleManager.hostRootDir = this.hostRootDir;
21✔
94
        this.moduleManager.noprefixNpmAliases = this.hostPackageJson?.ropm?.noprefix ?? [];
21!
95

96
        //copy all of them at once, wait for them all to complete
97
        for (const modulePath of modulePaths) {
21✔
98
            this.moduleManager.addModule(modulePath);
21✔
99
        }
100

101
        await this.moduleManager.process();
21✔
102
    }
103

104
    /**
105
     * Get the list of prod dependencies from npm.
106
     * This is run sync because it should run as fast as possible
107
     * and won't be run in ~parallel.
108
     */
109
    public getProdDependencies() {
110
        if (fsExtra.pathExistsSync(this.cwd) === false) {
28!
111
            throw new Error(`"${this.cwd}" does not exist`);
×
112
        }
113
        let stdout: string;
114
        try {
28✔
115
            stdout = childProcess.execSync('npm ls --json --long --omit=dev --depth=Infinity', {
28✔
116
                cwd: this.cwd
117
            }).toString();
118
        } catch (e: any) {
119
            stdout = e.stdout.toString();
1✔
120
            const stderr: string = e.stderr.toString();
1✔
121
            //sometimes the unit tests absorb stderr...so as long as we have stdout, assume it's valid (and ignore the stderr)
122
            if (stderr.includes('npm ERR! extraneous:')) {
1!
123
                //ignore errors
124
            } else {
125
                throw new Error('Failed to compute prod dependencies: ' + e.message);
1✔
126
            }
127
        }
128

129
        const dependencyJson = JSON.parse(stdout);
27✔
130
        const thisPackage = this.findDependencyByName(dependencyJson, this.hostPackageJson?.name);
27✔
131
        const dependencies = this.flattenPackage(thisPackage);
27✔
132
        return dependencies;
27✔
133
    }
134

135
    /**
136
     * Flatten dependencies from `npm ls --json --long` to match the parseable output
137
     * @param packageJson the result from `npm ls --json --long`
138
     * @returns list of dependency paths
139
     */
140
    private flattenPackage(packageJson: any): string[] {
141
        const dependencies: string[] = [];
56✔
142
        if (packageJson) {
56!
143
            dependencies.push(packageJson.path);
56✔
144
            for (const dep of Object.values(packageJson.dependencies ?? {})) {
56!
145
                dependencies.push(...this.flattenPackage(dep));
29✔
146
            }
147
        }
148
        return dependencies;
56✔
149
    }
150

151
    /**
152
     * Finds the current package in the dependency tree
153
     *
154
     * Important for workspace projects, where the root project
155
     * is included in the dependency tree and needs to be removed
156
     * @param packageJson root depenendency json
157
     * @param name cwd project name
158
     * @returns package entry in dependency json
159
     */
160
    private findDependencyByName(packageJson: any, name: string | undefined) {
161
        if (packageJson.name === name || !name) {
27!
162
            return packageJson;
27✔
163
        }
164
        let foundPackage = Object.values(packageJson.dependencies ?? {}).find((d: any) => d.name === name);
×
165
        if (!foundPackage) {
×
166
            for (const key in (packageJson.dependencies ?? {})) {
×
167
                foundPackage = this.findDependencyByName(packageJson.dependencies[key], name);
×
168
                if (foundPackage) {
×
169
                    return foundPackage;
×
170
                }
171
            }
172
        }
173
        return foundPackage;
×
174
    }
175
}
176

177
export interface InstallCommandArgs {
178
    /**
179
     * The current working directory for the command.
180
     */
181
    cwd?: string;
182
    /**
183
     * The list of packages that should be installed
184
     */
185
    packages?: string[];
186
    /**
187
     * Dependencies installation location.
188
     * By default the setting from package.json is imported out-of-the-box, but if rootDir is passed here,
189
     * it will override the value from package.json.
190
     */
191
    rootDir?: string;
192
}
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

© 2025 Coveralls, Inc