-
-
Notifications
You must be signed in to change notification settings - Fork 92
/
index.js
213 lines (200 loc) · 5.73 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import {resolve} from 'path'
import {readFileSync} from 'fs'
import {
includes,
isPlainObject,
isUndefined,
isEmpty,
isFunction,
} from 'lodash'
import typeOf from 'type-detect'
import chalk from 'chalk'
import {safeLoad} from 'js-yaml'
import {oneLine} from 'common-tags'
import getLogger from '../get-logger'
import {resolveScriptObjectToScript} from '../resolve-script-object-to-string'
import initialize from './initialize'
const log = getLogger()
/**
* Attempts to load the given module. This is used for the
* --require functionality of the CLI
* @param {String} moduleName The module to attempt to require
* @return {*} The required module
*/
const preloadModule = getAttemptModuleRequireFn((moduleName, requirePath) => {
log.warn({
message: chalk.yellow(
oneLine`
Unable to preload "${moduleName}".
Attempted to require as "${requirePath}"
`,
),
ref: 'unable-to-preload-module',
})
return undefined
})
const loadJSConfig = getAttemptModuleRequireFn(function onFail(
configPath,
requirePath,
err,
) {
if (err) {
throw err
}
log.error({
message: chalk.red(
oneLine`
Unable to find JS config at "${configPath}".
`,
),
ref: 'unable-to-find-config',
})
return undefined
})
/**
* Attempts to load the config and logs an error if there's a problem
* @param {String} configPath The path to attempt to require the config from
* @param {*} input the input to pass to the config if it's a function
* @return {Object} The config
*/
function loadConfig(configPath, input) {
if (configPath.endsWith('.yml')) {
return loadYAMLConfig(configPath)
}
let config = loadJSConfig(configPath)
if (isUndefined(config)) {
// let the caller deal with this
return config
}
let typeMessage = `Your config data type was`
if (isFunction(config)) {
config = config(input)
typeMessage = `${typeMessage} a function which returned`
}
const emptyConfig = isEmpty(config)
const plainObjectConfig = isPlainObject(config)
if (plainObjectConfig && emptyConfig) {
typeMessage = `${typeMessage} an object, but it was empty`
} else {
typeMessage = `${typeMessage} a data type of "${typeOf(config)}"`
}
if (!plainObjectConfig || emptyConfig) {
log.error({
message: chalk.red(
oneLine`
The package-scripts configuration
("${configPath.replace(/\\/g, '/')}") must be a non-empty object
or a function that returns a non-empty object.
`,
),
ref: 'config-must-be-an-object',
})
throw new Error(typeMessage)
}
return config
}
export {initialize, help, getModuleRequirePath, preloadModule, loadConfig}
/****** implementations ******/
function loadYAMLConfig(configPath) {
try {
return safeLoad(readFileSync(configPath, 'utf8'))
} catch (e) {
if (e.constructor.name === 'YAMLException') {
throw e
}
log.error({
message: chalk.red(`Unable to find YML config at "${configPath}".`),
ref: 'unable-to-find-config',
})
return undefined
}
}
/**
* Determines the proper require path for a module.
* If the path starts with `.` then it is resolved with process.cwd()
* @param {String} moduleName The module path
* @return {String} the module path to require
*/
function getModuleRequirePath(moduleName) {
return moduleName[0] === '.' ?
require.resolve(resolve(process.cwd(), moduleName)) :
moduleName
}
function getAttemptModuleRequireFn(onFail) {
return function attemptModuleRequire(moduleName) {
let requirePath
try {
requirePath = getModuleRequirePath(moduleName)
} catch (e) {
return onFail(moduleName)
}
try {
return requireDefaultFromModule(requirePath)
} catch (e) {
return onFail(moduleName, requirePath, e)
}
}
}
/**
* Requires the given module and returns the `default` if it's an `__esModule`
* @param {String} modulePath The module to require
* @return {*} The required module (or it's `default` if it's an `__esModule`)
*/
function requireDefaultFromModule(modulePath) {
/* eslint global-require:0,import/no-dynamic-require:0 */
const mod = require(modulePath)
if (mod.__esModule) {
return mod.default
} else {
return mod
}
}
function help({scripts}) {
const availableScripts = getAvailableScripts(scripts)
const filteredScripts = availableScripts.filter(
script => !script.hiddenFromHelp,
)
const scriptLines = filteredScripts.map(({name, description, script}) => {
const coloredName = chalk.green(name)
const coloredScript = chalk.gray(script)
let line
if (description) {
line = [coloredName, chalk.white(description), coloredScript]
} else {
line = [coloredName, coloredScript]
}
return line.join(' - ').trim()
})
if (scriptLines.length) {
const topMessage = 'Available scripts (camel or kebab case accepted)'
const message = `${topMessage}\n\n${scriptLines.join('\n')}`
return message
} else {
return chalk.yellow('There are no scripts available')
}
}
function getAvailableScripts(config, prefix = [], rootLevel = true) {
const excluded = ['description', 'script']
if (!rootLevel) {
excluded.push('default')
}
return Object.keys(config).reduce((scripts, key) => {
const val = config[key]
if (includes(excluded, key)) {
return scripts
}
const scriptObj = resolveScriptObjectToScript(val)
const prefixed = [...prefix, key]
if (scriptObj) {
const {description, script, hiddenFromHelp = false} = scriptObj
scripts = [
...scripts,
{name: prefixed.join('.'), description, script, hiddenFromHelp},
]
}
if (isPlainObject(val)) {
return [...scripts, ...getAvailableScripts(val, prefixed, false)]
}
return scripts
}, [])
}