Skip to content

Commit

Permalink
fix(load): allow function to be exported (as docs say)
Browse files Browse the repository at this point in the history
**What**: closes #88

**Why**: Because we document that you can export a function that returns
a configuration, but it doesn't actually work.

**How**: Add some logic to `loadConfig` in `src/bin-utils/index.js` to
handle the case when the config is a function. Also, we need to pass the
input along, so I had to make a few other changes as well. Also added
fixtures and tests for this.
  • Loading branch information
Kent C. Dodds committed Feb 8, 2017
1 parent 033bbda commit 626b059
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 37 deletions.
8 changes: 8 additions & 0 deletions other/ERRORS_AND_WARNINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,11 @@ This means that the child process for the specified script emitted an error.
Look at the error and try to figure out why the script would be failing.

Try to run the script without `p-s` and verify that the script is working. If not, fix that. If it's working without `p-s` it could be a problem with `p-s`. Please file an issue.

## Config Must be an Object

Your `package-scripts.js`, `package-scripts.yml`, or whatever you specified as the `--config` must be an object or a function that returns an object.

### To Fix:

Make sure that your config is an object or a function that returns an object.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"prefix-matches": "^0.0.9",
"readline-sync": "^1.4.6",
"shell-escape": "^0.2.0",
"spawn-command-with-kill": "^1.0.0"
"spawn-command-with-kill": "^1.0.0",
"type-detect": "^4.0.0"
},
"devDependencies": {
"all-contributors-cli": "^3.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/bin-utils/fixtures/bad-data-type-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'cannot be a string'
1 change: 1 addition & 0 deletions src/bin-utils/fixtures/bad-function-data-type-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = () => ['cannot be a function that returns an array']
5 changes: 5 additions & 0 deletions src/bin-utils/fixtures/function-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = () => ({
scripts: {
functionConfig: 'echo worked!',
}
})
34 changes: 29 additions & 5 deletions src/bin-utils/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {resolve} from 'path'
import {readFileSync} from 'fs'
import {remove, includes, isPlainObject, isEmpty} from 'lodash'
import {remove, includes, isPlainObject, isUndefined, isEmpty, isFunction} from 'lodash'
import typeOf from 'type-detect'
import shellEscape from 'shell-escape'
import colors from 'colors/safe'
import {safeLoad} from 'js-yaml'
Expand Down Expand Up @@ -35,15 +36,38 @@ const loadJSConfig = getAttemptModuleRequireFn(function onFail(configPath, requi

/**
* 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
* @return {*} The required module
* @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) {
function loadConfig(configPath, input) {
if (configPath.endsWith('.yml')) {
return loadYAMLConfig(configPath)
}

return loadJSConfig(configPath)
let config = loadJSConfig(configPath)
if (isUndefined(config)) {
// let the caller deal with this
return config
}
let typeMessage
if (isFunction(config)) {
config = config(input)
typeMessage = `Your config was a function that returned a data type of "${typeOf(config)}"`
} else {
typeMessage = `Your config data type was "${typeOf(config)}"`
}
if (!isPlainObject(config)) {
log.error({
message: colors.red(
`The package-scripts configuration ("${configPath}") ` +
'must be an object or a function that returns an object.',
),
ref: 'config-must-be-an-object',
})
throw new Error(typeMessage)
}
return config
}

export {
Expand Down
54 changes: 48 additions & 6 deletions src/bin-utils/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint import/newline-after-import:0, global-require:0 */
import {resolve} from 'path'
import path from 'path'
import colors from 'colors/safe'
import {spy} from 'sinon'
import {getScriptsAndArgs, help, preloadModule, loadConfig} from './index'
Expand Down Expand Up @@ -56,8 +56,7 @@ test('preloadModule: resolves a relative path', () => {
})

test('preloadModule: resolves an absolute path', () => {
const relativePath = './fixtures/my-module'
const absolutePath = resolve(__dirname, relativePath)
const absolutePath = getAbsoluteFixturePath('my-module')
const val = preloadModule(absolutePath)
expect(val).toBe('hello')
})
Expand All @@ -74,11 +73,50 @@ test('preloadModule: logs a warning when the module cannot be required', () => {
const {preloadModule: proxiedPreloadModule} = require('./index')
const val = proxiedPreloadModule('./module-that-does-exist')
expect(val).toBeUndefined()
expect(mockWarn.calledOnce)
expect(mockWarn.calledOnce).toBe(true)
const [{message}] = mockWarn.firstCall.args
expect(message).toMatch(/Unable to preload "\.\/module-that-does-exist"/)
})

test('loadConfig: calls the config function if it is a function', () => {
jest.resetModules()
const {loadConfig: proxiedLoadConfig} = require('./index')
const val = proxiedLoadConfig(getAbsoluteFixturePath('function-config.js'))
expect(val).toEqual({
scripts: {
functionConfig: 'echo worked!',
},
})
})

test('loadConfig: logs and throws an error for a config that exports the wrong data type', () => {
const mockError = jest.fn()
jest.resetModules()
jest.mock('../get-logger', () => () => ({error: mockError}))
const {loadConfig: proxiedLoadConfig} = require('./index')
const fixturePath = getAbsoluteFixturePath('bad-data-type-config.js')
expect(() => proxiedLoadConfig(fixturePath)).toThrowError(/Your config.*string/)
expect(mockError).toHaveBeenCalledTimes(1)
expect(mockError).toHaveBeenCalledWith({
message: expect.stringMatching(fixturePath),
ref: 'config-must-be-an-object',
})
})

test('loadConfig: logs and throws an error for a config that exports a function that returns the wrong data type', () => {
const mockError = jest.fn()
jest.resetModules()
jest.mock('../get-logger', () => () => ({error: mockError}))
const {loadConfig: proxiedLoadConfig} = require('./index')
const fixturePath = getAbsoluteFixturePath('bad-function-data-type-config.js')
expect(() => proxiedLoadConfig(fixturePath)).toThrowError(/Your config.*function.*Array/)
expect(mockError).toHaveBeenCalledTimes(1)
expect(mockError).toHaveBeenCalledWith({
message: expect.stringMatching(fixturePath),
ref: 'config-must-be-an-object',
})
})

test('loadConfig: logs a warning when the JS module cannot be required', () => {
const mockError = spy()
jest.resetModules()
Expand All @@ -93,7 +131,7 @@ test('loadConfig: logs a warning when the JS module cannot be required', () => {

test('loadConfig: does not swallow JS syntax errors', () => {
const originalCwd = process.cwd
process.cwd = jest.fn(() => resolve(__dirname, '../..'))
process.cwd = jest.fn(() => path.resolve(__dirname, '../..'))
const relativePath = './src/bin-utils/fixtures/syntax-error-module'
expect(() => loadConfig(relativePath)).toThrowError()
process.cwd = originalCwd
Expand All @@ -112,7 +150,7 @@ test('loadConfig: can load ES6 module', () => {

test('loadConfig: does not swallow YAML syntax errors', () => {
const originalCwd = process.cwd
process.cwd = jest.fn(() => resolve(__dirname, '../..'))
process.cwd = jest.fn(() => path.resolve(__dirname, '../..'))
const relativePath = './src/bin-utils/fixtures/syntax-error-config.yml'
expect(() => loadConfig(relativePath)).toThrowError()
process.cwd = originalCwd
Expand Down Expand Up @@ -217,3 +255,7 @@ test('help: do not display scripts with flag hiddenFromHelp set to true', () =>
const expected = colors.yellow('There are no scripts available')
expect(message).toBe(expected)
})

function getAbsoluteFixturePath(fixture) {
return path.join(__dirname, 'fixtures', fixture)
}
8 changes: 4 additions & 4 deletions src/bin/nps.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ program
.on('--help', onHelp)
.parse(process.argv)

const scriptsAndArgs = getScriptsAndArgs(program)

if (shouldAutocomplete) {
autocomplete(getPSConfig())
} else if (shouldRun) {
const psConfig = getPSConfig()
const hasDefaultScript = !!psConfig.scripts.default
const scriptsAndArgs = getScriptsAndArgs(program)
const hasHelpScript = !!psConfig.scripts.help
const scriptIsHelp = scriptsAndArgs.scripts[0] === 'help'
const scriptSpecified = scriptsAndArgs.scripts.length >= 1
Expand All @@ -47,11 +47,11 @@ if (shouldAutocomplete) {
} else if (!hasHelpScript && scriptIsHelp) { // eslint-disable-line no-negated-condition
program.outputHelp()
} else {
loadAndRun(scriptsAndArgs, psConfig)
loadAndRun(psConfig)
}
}

function loadAndRun(scriptsAndArgs, psConfig) {
function loadAndRun(psConfig) {
runPackageScript({
scriptConfig: psConfig.scripts,
scripts: scriptsAndArgs.scripts,
Expand All @@ -76,7 +76,7 @@ function getPSConfig() {
log.warn(colors.yellow('Unable to find a config file and none was specified.'))
return {scripts: {}} // empty config
}
const config = loadConfig(configFilepath)
const config = loadConfig(configFilepath, scriptsAndArgs)
if (!config) {
process.exit(FAIL_CODE)
}
Expand Down
52 changes: 31 additions & 21 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ align-text@^0.1.1, align-text@^0.1.3:
repeat-string "^1.5.2"

all-contributors-cli@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/all-contributors-cli/-/all-contributors-cli-3.1.0.tgz#1575e0e7815a933b9fa743b0e214e8c1406beedc"
version "3.1.1"
resolved "https://registry.yarnpkg.com/all-contributors-cli/-/all-contributors-cli-3.1.1.tgz#ef3446020d06d1c85a11d045f7a788b87c9f46e4"
dependencies:
async "^2.0.0-rc.1"
inquirer "^0.12.0"
Expand Down Expand Up @@ -137,8 +137,8 @@ append-transform@^0.4.0:
default-require-extensions "^1.0.0"

aproba@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.0.4.tgz#2713680775e7614c8ba186c065d4e2e52d1072c0"
version "1.1.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.0.tgz#4d8f047a318604e18e3c06a0e52230d3d19f147b"

are-we-there-yet@~1.0.0:
version "1.0.6"
Expand Down Expand Up @@ -272,8 +272,8 @@ aws-sign2@~0.6.0:
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"

aws4@^1.2.1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755"
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"

babel-cli@^6.22.2:
version "6.22.2"
Expand Down Expand Up @@ -954,8 +954,8 @@ braces@^1.8.2:
repeat-element "^1.1.2"

brorand@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5"
version "1.0.7"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.7.tgz#6677fa5e4901bdbf9c9ec2a748e28dca407a9bfc"

browser-resolve@^1.11.2:
version "1.11.2"
Expand Down Expand Up @@ -1572,8 +1572,8 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"

elliptic@^6.0.0:
version "6.3.2"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.2.tgz#e4c81e0829cf0a65ab70e998b8232723b5c1bc48"
version "6.3.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.3.tgz#5482d9646d54bcb89fd7d994fc9e2e9568876e3f"
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
Expand Down Expand Up @@ -1834,6 +1834,10 @@ esprima@^2.6.0, esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"

esprima@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"

esprima@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9"
Expand Down Expand Up @@ -2169,8 +2173,8 @@ gauge@~1.2.0:
lodash.padstart "^4.1.0"

gauge@~2.7.1:
version "2.7.2"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.2.tgz#15cecc31b02d05345a5d6b0e171cdb3ad2307774"
version "2.7.3"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.3.tgz#1c23855f962f17b3ad3d0dc7443f304542edfe09"
dependencies:
aproba "^1.0.3"
console-control-strings "^1.0.0"
Expand All @@ -2179,7 +2183,6 @@ gauge@~2.7.1:
signal-exit "^3.0.0"
string-width "^1.0.1"
strip-ansi "^3.0.1"
supports-color "^0.2.0"
wide-align "^1.1.0"

generate-function@^2.0.0:
Expand Down Expand Up @@ -2960,13 +2963,20 @@ js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"

js-yaml@3.7.0, js-yaml@^3.5.1, js-yaml@^3.7.0:
js-yaml@3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
dependencies:
argparse "^1.0.7"
esprima "^2.6.0"

js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.8.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
dependencies:
argparse "^1.0.7"
esprima "^3.1.1"

jsbn@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd"
Expand Down Expand Up @@ -4131,7 +4141,7 @@ regenerator-transform@0.9.8:

regex-cache@^0.4.2:
version "0.4.3"
resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145"
resolved "http://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145"
dependencies:
is-equal-shallow "^0.1.3"
is-primitive "^2.0.0"
Expand Down Expand Up @@ -4358,8 +4368,8 @@ sane@~1.4.1:
watch "~0.10.0"

sax@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
version "1.2.2"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"

semantic-release@^6.3.6:
version "6.3.6"
Expand Down Expand Up @@ -4652,10 +4662,6 @@ strip-json-comments@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"

supports-color@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"

supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
Expand Down Expand Up @@ -4819,6 +4825,10 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"

type-detect@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.0.tgz#62053883542a321f2f7b25746dc696478b18ff6b"

typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
Expand Down

0 comments on commit 626b059

Please sign in to comment.