Skip to content

Commit 6b15751

Browse files
Yash-Singh1ruyadorno
authored andcommitted
feat: add npm set-script
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
1 parent bc655b1 commit 6b15751

File tree

6 files changed

+246
-2
lines changed

6 files changed

+246
-2
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: npm-set-script
3+
section: 1
4+
description: Set tasks in the scripts section of package.json
5+
---
6+
7+
### Synopsis
8+
An npm command that lets you create a task in the scripts section of the package.json.
9+
10+
```bash
11+
npm set-script [<script>] [<command>]
12+
```
13+
14+
15+
**Example:**
16+
17+
* `npm set-script start "http-server ."`
18+
19+
```json
20+
{
21+
"name": "my-project",
22+
"scripts": {
23+
"start": "http-server .",
24+
"test": "some existing value"
25+
}
26+
}
27+
```
28+
29+
### See Also
30+
31+
* [npm run-script](/commands/npm-run-script)
32+
* [npm install](/commands/npm-install)
33+
* [npm test](/commands/npm-test)
34+
* [npm start](/commands/npm-start)

lib/set-script.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict'
2+
3+
const log = require('npmlog')
4+
const usageUtil = require('./utils/usage.js')
5+
const { localPrefix } = require('./npm.js')
6+
const fs = require('fs')
7+
const usage = usageUtil('set-script', 'npm set-script [<script>] [<command>]')
8+
const completion = require('./utils/completion/none.js')
9+
const parseJSON = require('json-parse-even-better-errors')
10+
const rpj = require('read-package-json-fast')
11+
12+
const cmd = (args, cb) => set(args).then(() => cb()).catch(cb)
13+
14+
const set = async function (args) {
15+
if (process.env.npm_lifecycle_event === 'postinstall')
16+
throw new Error('Scripts can’t set from the postinstall script')
17+
18+
// Parse arguments
19+
if (args.length !== 2)
20+
throw new Error(`Expected 2 arguments: got ${args.length}`)
21+
22+
// Set the script
23+
let manifest
24+
let warn = false
25+
try {
26+
manifest = fs.readFileSync(localPrefix + '/package.json', 'utf-8')
27+
} catch (error) {
28+
throw new Error('package.json not found')
29+
}
30+
try {
31+
manifest = parseJSON(manifest)
32+
} catch (error) {
33+
throw new Error(`Invalid package.json: ${error}`)
34+
}
35+
if (!manifest.scripts)
36+
manifest.scripts = {}
37+
if (manifest.scripts[args[0]] && manifest.scripts[args[0]] !== args[1])
38+
warn = true
39+
manifest.scripts[args[0]] = args[1]
40+
// format content
41+
const packageJsonInfo = await rpj(localPrefix + '/package.json')
42+
const {
43+
[Symbol.for('indent')]: indent,
44+
[Symbol.for('newline')]: newline,
45+
} = packageJsonInfo
46+
const format = indent === undefined ? ' ' : indent
47+
const eol = newline === undefined ? '\n' : newline
48+
const content = (JSON.stringify(manifest, null, format) + '\n')
49+
.replace(/\n/g, eol)
50+
fs.writeFileSync(localPrefix + '/package.json', content)
51+
if (warn)
52+
log.warn('set-script', `Script "${args[0]}" was overwritten`)
53+
}
54+
55+
module.exports = Object.assign(cmd, { usage, completion })

lib/utils/cmd-list.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const cmdList = [
127127
'start',
128128
'restart',
129129
'run-script',
130+
'set-script',
130131
'completion',
131132
'doctor',
132133
'exec',

tap-snapshots/test-lib-utils-cmd-list.js-TAP.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ Object {
162162
"start",
163163
"restart",
164164
"run-script",
165+
"set-script",
165166
"completion",
166167
"doctor",
167168
"exec",

test/coverage-map.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ const coverageMap = (filename) => {
99
return glob.sync(`${dir}/**/*.js`)
1010
.map(f => relative(process.cwd(), f))
1111
}
12-
if (/^test\/(lib|bin)\//.test(filename)) {
12+
if (/^test\/(lib|bin)\//.test(filename))
1313
return filename.replace(/^test\//, '')
14-
}
1514
return []
1615
}
1716

test/lib/set-script.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
const test = require('tap')
2+
const requireInject = require('require-inject')
3+
const setScriptDefault = require('../../lib/set-script.js')
4+
const parseJSON = require('json-parse-even-better-errors')
5+
6+
test.type(setScriptDefault, 'function', 'command is function')
7+
test.equal(setScriptDefault.completion, require('../../lib/utils/completion/none.js'), 'empty completion')
8+
test.equal(setScriptDefault.usage, 'npm set-script [<script>] [<command>]', 'usage matches')
9+
test.test('fails on invalid arguments', (t) => {
10+
const setScript = requireInject('../../lib/set-script.js', {
11+
fs: {},
12+
npmlog: {},
13+
})
14+
t.plan(3)
15+
setScript(['arg1'], (fail) => t.match(fail, /Expected 2 arguments: got 1/))
16+
setScript(['arg1', 'arg2', 'arg3'], (fail) => t.match(fail, /Expected 2 arguments: got 3/))
17+
setScript(['arg1', 'arg2', 'arg3', 'arg4'], (fail) => t.match(fail, /Expected 2 arguments: got 4/))
18+
})
19+
test.test('fails if run in postinstall script', (t) => {
20+
var originalVar = process.env.npm_lifecycle_event
21+
process.env.npm_lifecycle_event = 'postinstall'
22+
const setScript = requireInject('../../lib/set-script.js', {
23+
fs: {},
24+
npmlog: {},
25+
})
26+
t.plan(1)
27+
setScript(['arg1', 'arg2'], (fail) => t.equal(fail.toString(), 'Error: Scripts can’t set from the postinstall script'))
28+
process.env.npm_lifecycle_event = originalVar
29+
})
30+
test.test('fails when package.json not found', (t) => {
31+
const setScript = requireInject('../../lib/set-script.js', {
32+
'../../lib/npm.js': {
33+
localPrefix: 'IDONTEXIST',
34+
},
35+
})
36+
t.plan(1)
37+
setScript(['arg1', 'arg2'], (fail) => t.match(fail, /package.json not found/))
38+
})
39+
test.test('fails on invalid JSON', (t) => {
40+
const setScript = requireInject('../../lib/set-script.js', {
41+
fs: {
42+
readFileSync: (name, charcode) => {
43+
return 'iamnotjson'
44+
},
45+
},
46+
})
47+
t.plan(1)
48+
setScript(['arg1', 'arg2'], (fail) => t.match(fail, /Invalid package.json: JSONParseError/))
49+
})
50+
test.test('creates scripts object', (t) => {
51+
var mockFile = ''
52+
const setScript = requireInject('../../lib/set-script.js', {
53+
fs: {
54+
readFileSync: (name, charcode) => {
55+
return '{}'
56+
},
57+
writeFileSync: (location, inner) => {
58+
mockFile = inner
59+
},
60+
},
61+
'read-package-json-fast': async function (filename) {
62+
return {
63+
[Symbol.for('indent')]: ' ',
64+
[Symbol.for('newline')]: '\n',
65+
}
66+
},
67+
})
68+
t.plan(2)
69+
setScript(['arg1', 'arg2'], (error) => {
70+
t.equal(error, undefined)
71+
t.assert(parseJSON(mockFile), {scripts: {arg1: 'arg2'}})
72+
})
73+
})
74+
test.test('warns before overwriting', (t) => {
75+
var warningListened = ''
76+
const setScript = requireInject('../../lib/set-script.js', {
77+
fs: {
78+
readFileSync: (name, charcode) => {
79+
return JSON.stringify({
80+
scripts: {
81+
arg1: 'blah',
82+
},
83+
})
84+
},
85+
writeFileSync: (name, content) => {},
86+
},
87+
'read-package-json-fast': async function (filename) {
88+
return {
89+
[Symbol.for('indent')]: ' ',
90+
[Symbol.for('newline')]: '\n',
91+
}
92+
},
93+
npmlog: {
94+
warn: (prefix, message) => {
95+
warningListened = message
96+
},
97+
},
98+
})
99+
t.plan(2)
100+
setScript(['arg1', 'arg2'], (error) => {
101+
t.equal(error, undefined, 'no error')
102+
t.equal(warningListened, 'Script "arg1" was overwritten')
103+
})
104+
})
105+
test.test('provided indentation and eol is used', (t) => {
106+
var mockFile = ''
107+
const setScript = requireInject('../../lib/set-script.js', {
108+
fs: {
109+
readFileSync: (name, charcode) => {
110+
return '{}'
111+
},
112+
writeFileSync: (name, content) => {
113+
mockFile = content
114+
},
115+
},
116+
'read-package-json-fast': async function (filename) {
117+
return {
118+
[Symbol.for('indent')]: ' '.repeat(6),
119+
[Symbol.for('newline')]: '\r\n',
120+
}
121+
},
122+
})
123+
t.plan(3)
124+
setScript(['arg1', 'arg2'], (error) => {
125+
t.equal(error, undefined)
126+
t.equal(mockFile.split('\r\n').length > 1, true)
127+
t.equal(mockFile.split('\r\n').every((value) => !value.startsWith(' ') || value.startsWith(' '.repeat(6))), true)
128+
})
129+
})
130+
test.test('goes to default when undefined indent and eol provided', (t) => {
131+
var mockFile = ''
132+
const setScript = requireInject('../../lib/set-script.js', {
133+
fs: {
134+
readFileSync: (name, charcode) => {
135+
return '{}'
136+
},
137+
writeFileSync: (name, content) => {
138+
mockFile = content
139+
},
140+
},
141+
'read-package-json-fast': async function (filename) {
142+
return {
143+
[Symbol.for('indent')]: undefined,
144+
[Symbol.for('newline')]: undefined,
145+
}
146+
},
147+
})
148+
t.plan(3)
149+
setScript(['arg1', 'arg2'], (error) => {
150+
t.equal(error, undefined)
151+
t.equal(mockFile.split('\n').length > 1, true)
152+
t.equal(mockFile.split('\n').every((value) => !value.startsWith(' ') || value.startsWith(' ')), true)
153+
})
154+
})

0 commit comments

Comments
 (0)