UNPKG

7.37 kBJavaScriptView Raw
1// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
2// Node module: @loopback/build
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7
8const util = require('util');
9const fs = require('fs');
10const path = require('path');
11const spawn = require('cross-spawn');
12const debug = require('debug')('loopback:build');
13
14/**
15 * Get the root directory of this module
16 */
17function getRootDir() {
18 return path.resolve(__dirname, '..');
19}
20
21/**
22 * Get the root directory of the npm package
23 */
24function getPackageDir() {
25 return process.cwd();
26}
27
28/**
29 * Get config file
30 * @param {string} name Preferred file
31 * @param {string} defaultName Default file
32 */
33function getConfigFile(name, defaultName) {
34 const dir = getPackageDir();
35 let configFile = path.join(dir, name);
36 if (!fs.existsSync(configFile)) {
37 debug('%s does not exist', configFile);
38 if (defaultName) {
39 configFile = path.join(dir, defaultName);
40 if (!fs.existsSync(configFile)) {
41 debug('%s does not exist', configFile);
42 configFile = path.join(getRootDir(), 'config/' + name);
43 } else {
44 debug('%s found', configFile);
45 }
46 } else {
47 // Fall back to config/
48 configFile = path.join(getRootDir(), 'config/' + name);
49 debug('%s found', configFile);
50 }
51 } else {
52 debug('%s found', configFile);
53 }
54 return configFile;
55}
56
57/**
58 * Resolve the path of the cli command
59 * @param {string} cli CLI module path
60 * @param {object} options Options for module resolution
61 * - `resolveFromProjectFirst`: Try to resolve the CLI path from the package
62 * dependencies of the current project first instead of `@loopback/build`
63 */
64function resolveCLI(cli, options = {resolveFromProjectFirst: true}) {
65 const {resolveFromProjectFirst = true} = options;
66 if (resolveFromProjectFirst === false) {
67 return require.resolve(cli);
68 }
69 try {
70 const pkgDir = getPackageDir();
71 const resolved = resolveCLIFromProject(cli, pkgDir);
72 if (resolved != null) return resolved;
73 } catch (e) {
74 // Ignore errors
75 }
76 return require.resolve(cli);
77}
78
79/**
80 * Parse the package name from the cli module path
81 * @param {string} cli CLI module path, such as `typescript/lib/tsc`
82 */
83function getPackageName(cli) {
84 const paths = cli.split('/');
85 if (paths[0].startsWith('@')) {
86 // The package name is `@<org>/<pkg>`
87 return `${paths[0]}/${paths[1]}`;
88 } else {
89 // The package name is `<pkg>`
90 return paths[0];
91 }
92}
93
94/**
95 * Resolve the cli from the current project
96 * @param {string} cli CLI module path, such as `typescript/lib/tsc`
97 * @param {string} projectRootDir The root directory for the project
98 */
99function resolveCLIFromProject(cli, projectRootDir = getPackageDir()) {
100 const cliPkg = getPackageName(cli);
101 debug(`Trying to resolve CLI module '%s' from %s`, cliPkg, projectRootDir);
102 // Try to load `package.json` for the current project
103 const pkg = require(path.join(projectRootDir, 'package.json'));
104 if (
105 (pkg.devDependencies && pkg.devDependencies[cliPkg]) ||
106 (pkg.dependencies && pkg.dependencies[cliPkg])
107 ) {
108 // The cli package is found in the project dependencies
109 const modulePath = './node_modules/' + cli;
110 const cliModule = require.resolve(path.join(projectRootDir, modulePath));
111 debug(`Resolved CLI module '%s': %s`, cliPkg, cliModule);
112 return cliModule;
113 } else {
114 debug(`CLI module '%s' is not found in dependencies`, cliPkg);
115 }
116}
117
118/**
119 * Run a command with the given arguments
120 * @param {string} cli Path of the cli command
121 * @param {string[]} args The arguments
122 * @param {object} options Options to control dryRun and spawn
123 * - nodeArgs An array of args for `node`
124 * - dryRun Controls if the cli will be executed or not. If set
125 * to true, the command itself will be returned without running it
126 */
127function runCLI(cli, args, options) {
128 cli = resolveCLI(cli, options && options.resolveFromProjectFirst);
129 args = [cli].concat(args);
130 debug('%s', args.join(' '));
131 // Keep it backward compatible as dryRun
132 if (typeof options === 'boolean') options = {dryRun: options};
133 options = options || {};
134 if (options.dryRun) {
135 return util.format('%s %s', process.execPath, args.join(' '));
136 }
137 if (options.nodeArgs) {
138 debug('node args: %s', options.nodeArgs.join(' '));
139 // For example, [--no-warnings]
140 args = options.nodeArgs.concat(args);
141 }
142 debug('Spawn %s %s', process.execPath, args.join(' '));
143 const child = spawn(
144 process.execPath, // Typically '/usr/local/bin/node'
145 args,
146 Object.assign(
147 {
148 stdio: 'inherit',
149 env: Object.create(process.env),
150 },
151 options,
152 ),
153 );
154 child.on('close', (code, signal) => {
155 debug('%s exits: %d', cli, code);
156 process.exitCode = code;
157 });
158 return child;
159}
160
161/**
162 * Run the command in a shell
163 * @param {string} command The command
164 * @param {string[]} args The arguments
165 * @param {object} options Options to control dryRun and spawn
166 * - dryRun Controls if the cli will be executed or not. If set
167 * to true, the command itself will be returned without running it
168 */
169function runShell(command, args, options) {
170 args = args.map(a => JSON.stringify(a));
171 debug('%s %s', command, args.join(' '));
172 // Keep it backward compatible as dryRun
173 if (typeof options === 'boolean') options = {dryRun: options};
174 options = options || {};
175 if (options.dryRun) {
176 return util.format('%s %s', command, args.join(' '));
177 }
178 const child = spawn(
179 command,
180 args,
181 Object.assign(
182 {
183 stdio: 'inherit',
184 env: Object.create(process.env),
185 // On Windows, npm creates `.cmd` files instead of symlinks in
186 // `node_modules/.bin` folder. These files cannot be executed directly,
187 // only via a shell.
188 shell: true,
189 },
190 options,
191 ),
192 );
193 child.on('close', (code, signal) => {
194 debug('%s exits: %d', command, code);
195 if (code > 0 || signal) {
196 console.warn(
197 'Command aborts (code %d signal %s): %s %s.',
198 code,
199 signal,
200 command,
201 args.join(' '),
202 );
203 }
204 if (signal === 'SIGKILL' && code === 0) {
205 // Travis might kill processes under resource pressure
206 code = 128;
207 }
208 process.exitCode = code;
209 });
210 return child;
211}
212
213/**
214 * Check if one of the option names is set by the opts
215 * @param {string[]} opts
216 * @param {string[]} optionNames
217 */
218function isOptionSet(opts, ...optionNames) {
219 return opts.some(opt =>
220 // It can be --my-opt or --my-opt=my-value
221 optionNames.some(name => name === opt || opt.startsWith(`${name}=`)),
222 );
223}
224
225function mochaConfiguredForProject() {
226 const configFiles = [
227 '.mocharc.js',
228 '.mocharc.json',
229 '.mocharc.yaml',
230 '.mocharc.yml',
231 ];
232 return configFiles.some(f => {
233 const configFile = path.join(getPackageDir(), f);
234 return fs.existsSync(configFile);
235 });
236}
237
238exports.getRootDir = getRootDir;
239exports.getPackageDir = getPackageDir;
240exports.getConfigFile = getConfigFile;
241exports.resolveCLI = resolveCLI;
242exports.runCLI = runCLI;
243exports.runShell = runShell;
244exports.isOptionSet = isOptionSet;
245exports.mochaConfiguredForProject = mochaConfiguredForProject;
246exports.resolveCLIFromProject = resolveCLIFromProject;
247exports.getPackageName = getPackageName;