Skip to content

Commit c7fe1c7

Browse files
committedApr 12, 2023
fix: save raw data to file, not parsed data
When ${X} values are read from an rc file, those values should be written back as-is when config is re-saved Fixes #6183
1 parent 667cff5 commit c7fe1c7

File tree

5 files changed

+49
-14
lines changed

5 files changed

+49
-14
lines changed
 

‎docs/lib/content/commands/npm-config.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ npm set key=value [key=value...]
3535

3636
Sets each of the config keys to the value provided.
3737

38-
If value is omitted, then it sets it to an empty string.
38+
If value is omitted, the key will be removed from your config file entirely.
3939

4040
Note: for backwards compatibility, `npm config set key value` is supported
4141
as an alias for `npm config set key=value`.

‎lib/commands/config.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,13 @@ class Config extends BaseCommand {
163163
`The \`${baseKey}\` option is deprecated, and can not be set in this way${deprecated}`
164164
)
165165
}
166-
this.npm.config.set(key, val || '', where)
166+
167+
if (val === '') {
168+
this.npm.config.delete(key, where)
169+
} else {
170+
this.npm.config.set(key, val, where)
171+
}
172+
167173
if (!this.npm.config.validate(where)) {
168174
log.warn('config', 'omitting invalid config values')
169175
}

‎test/lib/commands/config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,13 @@ t.test('config set key1 value1 key2=value2 key3', async t => {
298298

299299
t.equal(sandbox.config.get('access'), 'restricted', 'access was set')
300300
t.equal(sandbox.config.get('all'), false, 'all was set')
301-
t.equal(sandbox.config.get('audit'), false, 'audit was set')
301+
t.equal(sandbox.config.get('audit'), true, 'audit was unset and restored to its default')
302302

303303
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
304304
const rc = ini.parse(contents)
305305
t.equal(rc.access, 'restricted', 'access is set to restricted')
306306
t.equal(rc.all, false, 'all is set to false')
307-
t.equal(rc.audit, false, 'audit is set to false')
307+
t.not(contents.includes('audit='), 'config file does not set audit')
308308
})
309309

310310
t.test('config set invalid key logs warning', async t => {

‎workspaces/config/lib/index.js

+16-10
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,7 @@ class Config {
169169
if (!this.loaded) {
170170
throw new Error('call config.load() before reading values')
171171
}
172-
// TODO single use?
173-
return this.#find(key)
174-
}
175172

176-
#find (key) {
177173
// have to look in reverse order
178174
const entries = [...this.data.entries()]
179175
for (let i = entries.length - 1; i > -1; i--) {
@@ -210,8 +206,11 @@ class Config {
210206
throw new Error('invalid config location param: ' + where)
211207
}
212208
this.#checkDeprecated(key)
213-
const { data } = this.data.get(where)
209+
const { data, raw } = this.data.get(where)
214210
data[key] = val
211+
if (['global', 'user', 'project'].includes(where)) {
212+
raw[key] = val
213+
}
215214

216215
// this is now dirty, the next call to this.valid will have to check it
217216
this.data.get(where)[_valid] = null
@@ -244,7 +243,11 @@ class Config {
244243
if (!confTypes.has(where)) {
245244
throw new Error('invalid config location param: ' + where)
246245
}
247-
delete this.data.get(where).data[key]
246+
const { data, raw } = this.data.get(where)
247+
delete data[key]
248+
if (['global', 'user', 'project'].includes(where)) {
249+
delete raw[key]
250+
}
248251
}
249252

250253
async load () {
@@ -537,6 +540,7 @@ class Config {
537540
}
538541

539542
#loadObject (obj, where, source, er = null) {
543+
// obj is the raw data read from the file
540544
const conf = this.data.get(where)
541545
if (conf.source) {
542546
const m = `double-loading "${where}" configs from ${source}, ` +
@@ -724,7 +728,9 @@ class Config {
724728
}
725729
}
726730

727-
const iniData = ini.stringify(conf.data).trim() + '\n'
731+
// We need the actual raw data before we called parseField so that we are
732+
// saving the same content back to the file
733+
const iniData = ini.stringify(conf.raw).trim() + '\n'
728734
if (!iniData.trim()) {
729735
// ignore the unlink error (eg, if file doesn't exist)
730736
await unlink(conf.source).catch(er => {})
@@ -873,7 +879,7 @@ class ConfigData {
873879
#raw = null
874880
constructor (parent) {
875881
this.#data = Object.create(parent && parent.data)
876-
this.#raw = null
882+
this.#raw = {}
877883
this[_valid] = true
878884
}
879885

@@ -897,7 +903,7 @@ class ConfigData {
897903
}
898904

899905
set loadError (e) {
900-
if (this[_loadError] || this.#raw) {
906+
if (this[_loadError] || (Object.keys(this.#raw).length)) {
901907
throw new Error('cannot set ConfigData loadError after load')
902908
}
903909
this[_loadError] = e
@@ -908,7 +914,7 @@ class ConfigData {
908914
}
909915

910916
set raw (r) {
911-
if (this.#raw || this[_loadError]) {
917+
if (Object.keys(this.#raw).length || this[_loadError]) {
912918
throw new Error('cannot set ConfigData raw after load')
913919
}
914920
this.#raw = r

‎workspaces/config/test/index.js

+23
Original file line numberDiff line numberDiff line change
@@ -1317,3 +1317,26 @@ t.test('workspaces', async (t) => {
13171317
t.match(logs[0], ['info', /^found workspace root at/], 'logged info about workspace root')
13181318
})
13191319
})
1320+
1321+
t.test('env-replaced config from files is not clobbered when saving', async (t) => {
1322+
const path = t.testdir()
1323+
const opts = {
1324+
shorthands: {},
1325+
argv: ['node', __filename, `--userconfig=${path}/.npmrc`],
1326+
env: { TEST: 'test value' },
1327+
definitions: {
1328+
registry: { default: 'https://registry.npmjs.org/' },
1329+
},
1330+
npmPath: process.cwd(),
1331+
}
1332+
const c = new Config(opts)
1333+
await c.load()
1334+
c.set('test', '${TEST}', 'user')
1335+
await c.save('user')
1336+
const d = new Config(opts)
1337+
await d.load()
1338+
d.set('other', '${SOMETHING}', 'user')
1339+
await d.save('user')
1340+
const rc = readFileSync(`${path}/.npmrc`, 'utf8')
1341+
t.match(rc, 'test=${TEST}', '${TEST} is present, not parsed')
1342+
})

0 commit comments

Comments
 (0)
Please sign in to comment.