Skip to content

Commit

Permalink
feat(bin-utils): do not move core scripts, convert npm run scripts in…
Browse files Browse the repository at this point in the history
…to (#154)

nps

* Core scripts ex. npm lifecycle scripts, husky hooks, will be not moved to
* package-scripts.js

* Scripts in package.json which contains npm run command will be converted
* to nps command
  • Loading branch information
Miklet authored and Kent C. Dodds committed Jul 28, 2017
1 parent 9d695d8 commit 6c0ee45
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`stringify object correctly 1`] = `
"
foo: 'a',
bar: {
baz: 'b'
}"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"scripts": {
"foo": "echo \"foo\"",
"precommit": "precommit hook",
"postcommit": "postcommit hook",
"prepublish": "prepublish hook",
"preuninstall": "preuninstall hook"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ module.exports = {
bar: 'echo prefoo:bar'
},
foo: {
default: 'nps prefoo && echo foo',
default: 'nps prefoo && nps "foo --bar=1"',
bar: {
default: 'nps prefoo.bar && echo foo:bar && nps postfoo.bar',
default: 'nps prefoo.bar && nps foo.bar && nps postfoo.bar',
baz: 'echo foo:bar:baz'
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ scripts:
default: echo prefoo
bar: 'echo prefoo:bar'
foo:
default: nps prefoo && echo foo
default: nps prefoo && nps "foo --bar=1"
bar:
default: 'nps prefoo.bar && echo foo:bar && nps postfoo.bar'
default: nps prefoo.bar && nps foo.bar && nps postfoo.bar
baz: 'echo foo:bar:baz'
bar: echo bar && nps postbar
postbar: echo postbar
Expand Down
4 changes: 2 additions & 2 deletions src/bin-utils/initialize/__tests__/fixtures/_package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"start:stuff": "echo start:stuff",
"test": "echo test",
"prefoo": "echo prefoo",
"foo": "echo foo",
"foo": "npm run foo -- --bar=1",
"bar": "echo bar",
"postbar": "echo postbar",
"prefoo-bar": "echo prefoo-bar",
"foo-bar": "echo foo-bar",
"foobar": "echo \"foo bar\"",
"baz": "echo 'baz buzz'",
"prefoo:bar": "echo prefoo:bar",
"foo:bar": "echo foo:bar",
"foo:bar": "npm run foo:bar",
"postfoo:bar": "echo postfoo:bar",
"foo:bar:baz": "echo foo:bar:baz"
}
Expand Down
27 changes: 27 additions & 0 deletions src/bin-utils/initialize/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ test(`initialize without any scripts should successfully create an empty package
})
})

test('initialize without any core scripts and should not remove any core scripts from package.json', () => {
const packageJsonDestination = fixture('_package-core-scripts.json')
const mockWriteFileSync = spy()
const mockFindUpSync = spy(file => {
if (file === 'package.json') {
return packageJsonDestination
}
throw new Error('Should not look for anything but package.json')
})
jest.resetModules()
jest.mock('find-up', () => ({sync: mockFindUpSync}))
jest.mock('fs', () => ({writeFileSync: mockWriteFileSync}))
const initialize = require('../').default

initialize()
const [, packageJsonStringResult] = mockWriteFileSync.firstCall.args
const {scripts: packageJsonScripts} = JSON.parse(packageJsonStringResult)

expect(packageJsonScripts).toEqual({
start: 'nps',
precommit: 'precommit hook',
postcommit: 'postcommit hook',
prepublish: 'prepublish hook',
preuninstall: 'preuninstall hook',
})
})

function fixture(name) {
return path.join(__dirname, 'fixtures', name)
}
13 changes: 13 additions & 0 deletions src/bin-utils/initialize/__tests__/stringify-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import stringifyObject from './../stringify-object'

const objectToStringify = {
foo: 'a',
bar: {
baz: 'b',
},
}

test('stringify object correctly', () => {
const stringObject = stringifyObject(objectToStringify, ' ')
expect(stringObject).toMatchSnapshot()
})
179 changes: 127 additions & 52 deletions src/bin-utils/initialize/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,68 @@
import {resolve, dirname} from 'path'
import {writeFileSync} from 'fs'
import {sync as findUpSync} from 'find-up'
import {isPlainObject, camelCase, set, each} from 'lodash'
import {
isPlainObject,
camelCase,
set,
each,
includes,
startsWith,
} from 'lodash'
import {safeDump} from 'js-yaml'
import stringifyObject from './stringify-object'

export default initialize

const CORE_SCRIPTS = [
'applypatchmsg',
'commitmsg',
'install',
'postapplypatch',
'postcheckout',
'postcommit',
'postinstall',
'postmerge',
'postpublish',
'postreceive',
'postrestart',
'postrewrite',
'poststart',
'poststop',
'posttest',
'postuninstall',
'postupdate',
'postversion',
'preapplypatch',
'preautogc',
'precommit',
'preinstall',
'preparecommitmsg',
'prepublish',
'prepush',
'prerebase',
'prereceive',
'prerestart',
'prestart',
'prestop',
'pretest',
'preuninstall',
'preversion',
'publish',
'pushtocheckout',
'restart',
'stop',
'uninstall',
'update',
'version',
]

function initialize(configType = 'js') {
/* eslint global-require:0,import/no-dynamic-require:0 */
const packageJsonPath = findUpSync('package.json')
const packageJson = require(packageJsonPath)
const {scripts = {}} = packageJson
packageJson.scripts = {
start: 'nps',
test: scripts.test ? 'nps test' : undefined,
}
packageJson.scripts = getCoreScripts(packageJson.scripts)
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))

if (configType === 'yaml') {
Expand Down Expand Up @@ -49,35 +97,41 @@ function dumpYAMLConfig(packageJsonPath, scripts) {
function generatePackageScriptsFileContents(scripts) {
const indent = ' ' // start at 4 spaces because we're inside another object
const structuredScripts = structureScripts(scripts)
const objectString = jsObjectStringify(structuredScripts, indent)
const objectString = stringifyObject(structuredScripts, indent)
return `module.exports = {\n scripts: {${objectString}\n }\n};\n`
}

function structureScripts(scripts) {
// start out by giving every script a `default`
const defaultedScripts = Object.keys(scripts).reduce((obj, scriptKey) => {
const keyParts = scriptKey.split(':')
const isKeyScriptHook = isScriptHook(keyParts[0])
const deepKey = keyParts.map(key => camelCase(key)).join('.')
let defaultDeepKey = `${deepKey}.default`
if (scriptKey.indexOf('start') === 0) {
defaultDeepKey = [
'default',
...keyParts.slice(1, keyParts.length),
'default',
].join('.')
}
let script = scripts[scriptKey]
if (!isKeyScriptHook) {
const preHook = scripts[`pre${scriptKey}`] ? `nps pre${deepKey} && ` : ''
const postHook = scripts[`post${scriptKey}`] ?
` && nps post${deepKey}` :
''
script = `${preHook}${script}${postHook}`
}
set(obj, defaultDeepKey, script)
return obj
}, {})
const defaultedScripts = Object.keys(scripts)
.filter(isNotCoreScript)
.reduce((obj, scriptKey) => {
const keyParts = scriptKey.split(':')
const isKeyScriptHook = isScriptHook(keyParts[0])
const deepKey = convertToNpsScript(keyParts)
let defaultDeepKey = `${deepKey}.default`
if (scriptKey.indexOf('start') === 0) {
defaultDeepKey = [
'default',
...keyParts.slice(1, keyParts.length),
'default',
].join('.')
}
let script = scripts[scriptKey]
if (!isKeyScriptHook) {
const {preHook, postHook} = getPrePostHooks(
scripts,
scriptKey,
deepKey,
)
if (isNpmRunCommand(script)) {
script = convertToNpsCommand(script)
}
script = `${preHook}${script}${postHook}`
}
set(obj, defaultDeepKey, script)
return obj
}, {})
// traverse the object and replace all objects that
// only have `default` with just the script itself.
traverse(defaultedScripts, removeDefaultOnly)
Expand All @@ -101,37 +155,58 @@ function traverse(object, fn) {
})
}

function jsObjectStringify(object, indent) {
return Object.keys(object).reduce(
(string, key, index) => {
const script = object[key]
let value
if (isPlainObject(script)) {
value = `{${jsObjectStringify(script, `${indent} `)}\n${indent}}`
} else {
value = `'${escapeSingleQuote(script)}'`
}
const comma = isLast(object, index) ? '' : ','
return `${string}\n${indent}${key}: ${value}${comma}`
},
'',
)
function getCoreScripts(scripts = {}) {
const DEFAULT_CORE_SCRIPTS = {
start: 'nps',
test: scripts.test ? 'nps test' : undefined,
}
const coreScripts = Object.keys(scripts).reduce((result, scriptKey) => {
if (!isNotCoreScript(scriptKey)) {
result[scriptKey] = scripts[scriptKey]
}
return result
}, {})
return Object.assign(DEFAULT_CORE_SCRIPTS, coreScripts)
}

function isOnlyDefault(script) {
return isPlainObject(script) &&
Object.keys(script).length === 1 &&
script.default
function convertToNpsCommand(npmRunCommand) {
const [, , commandToRun, , ...args] = npmRunCommand.split(' ')
const hasArgs = args.length > 0
let npsScript = convertToNpsScript(commandToRun.split(':'))
if (hasArgs) {
const npsScriptArgs = args.join(' ')
npsScript = `"${npsScript} ${npsScriptArgs}"`
}
return `nps ${npsScript}`
}

function escapeSingleQuote(string) {
return string.replace(/'/g, "\\'")
function getPrePostHooks(scripts, scriptKey, deepKey) {
const preHook = scripts[`pre${scriptKey}`] ? `nps pre${deepKey} && ` : ''
const postHook = scripts[`post${scriptKey}`] ? ` && nps post${deepKey}` : ''
return {
preHook,
postHook,
}
}

function isLast(object, index) {
return Object.keys(object).length - 1 === index
function convertToNpsScript(keyParts) {
return keyParts.map(key => camelCase(key)).join('.')
}

function isOnlyDefault(script) {
return (
isPlainObject(script) && Object.keys(script).length === 1 && script.default
)
}

function isScriptHook(script) {
return script.indexOf('pre') === 0 || script.indexOf('post') === 0
}

function isNotCoreScript(script) {
return !includes(CORE_SCRIPTS, script)
}

function isNpmRunCommand(script) {
return startsWith(script.trim(), 'npm run')
}
37 changes: 37 additions & 0 deletions src/bin-utils/initialize/stringify-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {isPlainObject} from 'lodash'

/**
* Converts given object to its string representation.
* Every line is preceded by given indent.
*
* @param {object} object "Object to convert"
* @param {string} indent "Indent to use"
* @returns {string} "Stringified and indented object"
*/
function stringifyObject(object, indent) {
return Object.keys(object).reduce((string, key, index) => {
const script = object[key]
let value
if (isPlainObject(script)) {
value = `{${stringifyObject(script, `${indent} `)}\n${indent}}`
} else {
value = `'${escapeSingleQuote(script)}'`
}
const comma = getComma(isLast(object, index))
return `${string}\n${indent}${key}: ${value}${comma}`
}, '')
}

function getComma(condition) {
return condition ? '' : ','
}

function isLast(object, index) {
return Object.keys(object).length - 1 === index
}

function escapeSingleQuote(string) {
return string.replace(/'/g, "\\'")
}

export default stringifyObject

0 comments on commit 6c0ee45

Please sign in to comment.