Skip to content

Commit

Permalink
feat: add npm set-script
Browse files Browse the repository at this point in the history
Introduces the set-script command. It accepts two arguments,
the script name and the command

ref: https://github.com/npm/rfcs/blob/latest/accepted/0016-set-script-command.md

PR-URL: #2237
Credit: @Yash-Singh1
Close: #2237
Reviewed-by: @ruyadorno
  • Loading branch information
Yash-Singh1 authored and ruyadorno committed Dec 4, 2020
1 parent bc655b1 commit 6b15751
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 2 deletions.
34 changes: 34 additions & 0 deletions docs/content/commands/npm-set-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: npm-set-script
section: 1
description: Set tasks in the scripts section of package.json
---

### Synopsis
An npm command that lets you create a task in the scripts section of the package.json.

```bash
npm set-script [<script>] [<command>]
```


**Example:**

* `npm set-script start "http-server ."`

```json
{
"name": "my-project",
"scripts": {
"start": "http-server .",
"test": "some existing value"
}
}
```

### See Also

* [npm run-script](/commands/npm-run-script)
* [npm install](/commands/npm-install)
* [npm test](/commands/npm-test)
* [npm start](/commands/npm-start)
55 changes: 55 additions & 0 deletions lib/set-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict'

const log = require('npmlog')
const usageUtil = require('./utils/usage.js')
const { localPrefix } = require('./npm.js')
const fs = require('fs')
const usage = usageUtil('set-script', 'npm set-script [<script>] [<command>]')
const completion = require('./utils/completion/none.js')
const parseJSON = require('json-parse-even-better-errors')
const rpj = require('read-package-json-fast')

const cmd = (args, cb) => set(args).then(() => cb()).catch(cb)

const set = async function (args) {
if (process.env.npm_lifecycle_event === 'postinstall')
throw new Error('Scripts can’t set from the postinstall script')

// Parse arguments
if (args.length !== 2)
throw new Error(`Expected 2 arguments: got ${args.length}`)

// Set the script
let manifest
let warn = false
try {
manifest = fs.readFileSync(localPrefix + '/package.json', 'utf-8')
} catch (error) {
throw new Error('package.json not found')
}
try {
manifest = parseJSON(manifest)
} catch (error) {
throw new Error(`Invalid package.json: ${error}`)
}
if (!manifest.scripts)
manifest.scripts = {}
if (manifest.scripts[args[0]] && manifest.scripts[args[0]] !== args[1])
warn = true
manifest.scripts[args[0]] = args[1]
// format content
const packageJsonInfo = await rpj(localPrefix + '/package.json')
const {
[Symbol.for('indent')]: indent,
[Symbol.for('newline')]: newline,
} = packageJsonInfo
const format = indent === undefined ? ' ' : indent
const eol = newline === undefined ? '\n' : newline
const content = (JSON.stringify(manifest, null, format) + '\n')
.replace(/\n/g, eol)
fs.writeFileSync(localPrefix + '/package.json', content)
if (warn)
log.warn('set-script', `Script "${args[0]}" was overwritten`)
}

module.exports = Object.assign(cmd, { usage, completion })
1 change: 1 addition & 0 deletions lib/utils/cmd-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const cmdList = [
'start',
'restart',
'run-script',
'set-script',
'completion',
'doctor',
'exec',
Expand Down
1 change: 1 addition & 0 deletions tap-snapshots/test-lib-utils-cmd-list.js-TAP.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Object {
"start",
"restart",
"run-script",
"set-script",
"completion",
"doctor",
"exec",
Expand Down
3 changes: 1 addition & 2 deletions test/coverage-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ const coverageMap = (filename) => {
return glob.sync(`${dir}/**/*.js`)
.map(f => relative(process.cwd(), f))
}
if (/^test\/(lib|bin)\//.test(filename)) {
if (/^test\/(lib|bin)\//.test(filename))
return filename.replace(/^test\//, '')
}
return []
}

Expand Down
154 changes: 154 additions & 0 deletions test/lib/set-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
const test = require('tap')
const requireInject = require('require-inject')
const setScriptDefault = require('../../lib/set-script.js')
const parseJSON = require('json-parse-even-better-errors')

test.type(setScriptDefault, 'function', 'command is function')
test.equal(setScriptDefault.completion, require('../../lib/utils/completion/none.js'), 'empty completion')
test.equal(setScriptDefault.usage, 'npm set-script [<script>] [<command>]', 'usage matches')
test.test('fails on invalid arguments', (t) => {
const setScript = requireInject('../../lib/set-script.js', {
fs: {},
npmlog: {},
})
t.plan(3)
setScript(['arg1'], (fail) => t.match(fail, /Expected 2 arguments: got 1/))
setScript(['arg1', 'arg2', 'arg3'], (fail) => t.match(fail, /Expected 2 arguments: got 3/))
setScript(['arg1', 'arg2', 'arg3', 'arg4'], (fail) => t.match(fail, /Expected 2 arguments: got 4/))
})
test.test('fails if run in postinstall script', (t) => {
var originalVar = process.env.npm_lifecycle_event
process.env.npm_lifecycle_event = 'postinstall'
const setScript = requireInject('../../lib/set-script.js', {
fs: {},
npmlog: {},
})
t.plan(1)
setScript(['arg1', 'arg2'], (fail) => t.equal(fail.toString(), 'Error: Scripts can’t set from the postinstall script'))
process.env.npm_lifecycle_event = originalVar
})
test.test('fails when package.json not found', (t) => {
const setScript = requireInject('../../lib/set-script.js', {
'../../lib/npm.js': {
localPrefix: 'IDONTEXIST',
},
})
t.plan(1)
setScript(['arg1', 'arg2'], (fail) => t.match(fail, /package.json not found/))
})
test.test('fails on invalid JSON', (t) => {
const setScript = requireInject('../../lib/set-script.js', {
fs: {
readFileSync: (name, charcode) => {
return 'iamnotjson'
},
},
})
t.plan(1)
setScript(['arg1', 'arg2'], (fail) => t.match(fail, /Invalid package.json: JSONParseError/))
})
test.test('creates scripts object', (t) => {
var mockFile = ''
const setScript = requireInject('../../lib/set-script.js', {
fs: {
readFileSync: (name, charcode) => {
return '{}'
},
writeFileSync: (location, inner) => {
mockFile = inner
},
},
'read-package-json-fast': async function (filename) {
return {
[Symbol.for('indent')]: ' ',
[Symbol.for('newline')]: '\n',
}
},
})
t.plan(2)
setScript(['arg1', 'arg2'], (error) => {
t.equal(error, undefined)
t.assert(parseJSON(mockFile), {scripts: {arg1: 'arg2'}})
})
})
test.test('warns before overwriting', (t) => {
var warningListened = ''
const setScript = requireInject('../../lib/set-script.js', {
fs: {
readFileSync: (name, charcode) => {
return JSON.stringify({
scripts: {
arg1: 'blah',
},
})
},
writeFileSync: (name, content) => {},
},
'read-package-json-fast': async function (filename) {
return {
[Symbol.for('indent')]: ' ',
[Symbol.for('newline')]: '\n',
}
},
npmlog: {
warn: (prefix, message) => {
warningListened = message
},
},
})
t.plan(2)
setScript(['arg1', 'arg2'], (error) => {
t.equal(error, undefined, 'no error')
t.equal(warningListened, 'Script "arg1" was overwritten')
})
})
test.test('provided indentation and eol is used', (t) => {
var mockFile = ''
const setScript = requireInject('../../lib/set-script.js', {
fs: {
readFileSync: (name, charcode) => {
return '{}'
},
writeFileSync: (name, content) => {
mockFile = content
},
},
'read-package-json-fast': async function (filename) {
return {
[Symbol.for('indent')]: ' '.repeat(6),
[Symbol.for('newline')]: '\r\n',
}
},
})
t.plan(3)
setScript(['arg1', 'arg2'], (error) => {
t.equal(error, undefined)
t.equal(mockFile.split('\r\n').length > 1, true)
t.equal(mockFile.split('\r\n').every((value) => !value.startsWith(' ') || value.startsWith(' '.repeat(6))), true)
})
})
test.test('goes to default when undefined indent and eol provided', (t) => {
var mockFile = ''
const setScript = requireInject('../../lib/set-script.js', {
fs: {
readFileSync: (name, charcode) => {
return '{}'
},
writeFileSync: (name, content) => {
mockFile = content
},
},
'read-package-json-fast': async function (filename) {
return {
[Symbol.for('indent')]: undefined,
[Symbol.for('newline')]: undefined,
}
},
})
t.plan(3)
setScript(['arg1', 'arg2'], (error) => {
t.equal(error, undefined)
t.equal(mockFile.split('\n').length > 1, true)
t.equal(mockFile.split('\n').every((value) => !value.startsWith(' ') || value.startsWith(' ')), true)
})
})

0 comments on commit 6b15751

Please sign in to comment.