Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions lib/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,6 @@ class Install extends ArboristWorkspaceCmd {
throw this.usageError()
}

const Arborist = require('@npmcli/arborist')
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const opts = {
...this.npm.flatOptions,
auditLevel: null,
path: where,
add: args,
workspaces: this.workspaceNames,
allowScripts: allowScriptsPolicy,
// patch relax flags are honored only when passed on the command line
...patchRelaxOpts(this.npm.config),
}

// Root lifecycle scripts only run for a bare `npm install` in a local project. `preinstall` runs *before* Arborist touches the filesystem so that scripts can bootstrap the environment (e.g. set up private-registry auth, generate files consumed during resolution) before dependencies are fetched or unpacked. The remaining scripts run after reify as they did before.
const runRootLifecycle = !args.length && !isGlobalInstall && !ignoreScripts
const runRootScript = (event) => runScript({
Expand All @@ -169,8 +156,21 @@ class Install extends ArboristWorkspaceCmd {

if (runRootLifecycle) {
await runRootScript('preinstall')
await this.npm.config.reload('user')
}

const Arborist = require('@npmcli/arborist')
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const opts = {
...this.npm.flatOptions,
auditLevel: null,
path: where,
add: args,
workspaces: this.workspaceNames,
allowScripts: allowScriptsPolicy,
// patch relax flags are honored only when passed on the command line
...patchRelaxOpts(this.npm.config),
}
const arb = new Arborist(opts)
await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: opts })
await arb.reify(opts)
Expand Down
5 changes: 5 additions & 0 deletions tap-snapshots/test/lib/commands/install.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ warn EBADDEVENGINES {
warn EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' },
warn EBADDEVENGINES required: { name: 'node', version: '0.0.1', onFail: 'warn' }
warn EBADDEVENGINES }
silly config load:file:{CWD}/home/.npmrc
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
silly idealTree buildDeps
silly reify moves {}
Expand Down Expand Up @@ -170,6 +171,7 @@ warn EBADDEVENGINES {
warn EBADDEVENGINES current: { name: 'x86' },
warn EBADDEVENGINES required: { name: 'risv', onFail: 'warn' }
warn EBADDEVENGINES }
silly config load:file:{CWD}/home/.npmrc
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
silly idealTree buildDeps
silly reify moves {}
Expand Down Expand Up @@ -256,6 +258,7 @@ warn EBADDEVENGINES {
warn EBADDEVENGINES current: { name: 'node', version: 'v1337.0.0' },
warn EBADDEVENGINES required: { name: 'nondescript' }
warn EBADDEVENGINES }
silly config load:file:{CWD}/home/.npmrc
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
silly idealTree buildDeps
silly reify moves {}
Expand All @@ -275,6 +278,7 @@ verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly"
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
silly logfile done cleaning log files
silly config load:file:{CWD}/home/.npmrc
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
silly idealTree buildDeps
silly reify moves {}
Expand All @@ -294,6 +298,7 @@ verbose argv "--fetch-retries" "0" "--cache" "{CWD}/cache" "--loglevel" "silly"
verbose logfile logs-max:10 dir:{CWD}/cache/_logs/{DATE}-
verbose logfile {CWD}/cache/_logs/{DATE}-debug-0.log
silly logfile done cleaning log files
silly config load:file:{CWD}/home/.npmrc
silly packumentCache heap:{heap} maxSize:{maxSize} maxEntrySize:{maxEntrySize}
silly idealTree buildDeps
warn EBADENGINE Unsupported engine {
Expand Down
34 changes: 34 additions & 0 deletions test/lib/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,40 @@ t.test('exec commands', async t => {
t.equal(post.depInstalled, true, 'postinstall runs after dependencies are installed')
})

await t.test('preinstall can refresh user registry credentials', async t => {
const tokenKey = '//registry.example/:_authToken'
let reifyToken
const { npm, home } = await loadMockNpm(t, {
config: { audit: false },
homeDir: {
'.npmrc': `${tokenKey}=expired-token`,
},
prefixDir: {
'package.json': JSON.stringify({
name: '@npmcli/test-package',
version: '1.0.0',
scripts: { preinstall: 'refresh credentials' },
}),
},
mocks: {
'@npmcli/run-script': async (opts) => {
if (opts.path === npm.prefix && opts.event === 'preinstall') {
fs.writeFileSync(path.join(home, '.npmrc'), `${tokenKey}=fresh-token`)
}
},
'@npmcli/arborist': function () {
this.reify = async opts => {
reifyToken = opts[tokenKey]
}
},
'{LIB}/utils/reify-finish.js': async () => {},
},
})

await npm.exec('install')
t.equal(reifyToken, 'fresh-token', 'reify uses credentials written by preinstall')
})

await t.test('without args, --ignore-scripts skips preinstall entirely', async t => {
const events = []
const { npm, registry } = await loadMockNpm(t, {
Expand Down
8 changes: 8 additions & 0 deletions workspaces/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ Load configuration from the various sources of information.

Returns a `Promise` that resolves when configuration is loaded, and fails if a fatal error is encountered.

### `config.reload(where)`

Reload an already-loaded `project`, `user`, or `global` configuration layer from its original file.
Values removed from the file are cleared, higher-priority layers keep their precedence, and the flattened options cache is invalidated.

Returns a `Promise` that resolves when the selected layer has been reloaded.
The initial `config.load()` must complete first.

### `config.find(key)`

Find the effective place in the configuration levels a given key is set.
Expand Down
27 changes: 27 additions & 0 deletions workspaces/config/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,23 @@ class Config {
this.setEnvs()
}

async reload (where) {
if (!this.loaded) {
throw new Error('call config.load() before reloading')
}
if (!confFileTypes.has(where)) {
throw new Error('invalid config location param: ' + where)
}

const conf = this.data.get(where)
const source = conf.source
this.sources.delete(source)
conf.reset()
this.#unknownConfigs = this.#unknownConfigs.filter(entry => entry.where !== where)
this.#flatOptions = null
await this.#loadFile(source, where)
}

loadDefaults () {
this.loadGlobalPrefix()
this.loadHome()
Expand Down Expand Up @@ -1094,6 +1111,16 @@ class ConfigData {
get raw () {
return this.#raw
}

reset () {
for (const key of Object.keys(this.#data)) {
delete this.#data[key]
}
this.#source = null
this.#raw = {}
this[_loadError] = null
this[_valid] = true
}
}

const getTypesFromDefinitions = (definitions) => {
Expand Down
45 changes: 45 additions & 0 deletions workspaces/config/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ t.test('construct with no settings, get default values for stuff', t => {
t.rejects(() => c.save('user'), {
message: 'call config.load() before saving',
})
t.rejects(() => c.reload('user'), {
message: 'call config.load() before reloading',
})
t.throws(() => c.data.set('user', {}), {
message: 'cannot change internal config data structure',
})
Expand Down Expand Up @@ -1061,6 +1064,48 @@ t.test('setting basic auth creds and email', async t => {
}, 'credentials saved and nerfed')
})

t.test('reload user config', async t => {
const registry = 'https://registry.example/'
const tokenKey = '//registry.example/:_authToken'
const path = t.testdir({
npm: { npmrc: '' },
project: { 'package.json': '{"name":"reload-user-config"}' },
user: { '.npmrc': `${tokenKey}=old-token\nfoo=from-user\n` },
})
const userconfig = join(path, 'user/.npmrc')
const config = new Config({
argv: ['node', __filename, `--userconfig=${userconfig}`],
cwd: join(path, 'project'),
definitions,
env: { HOME: join(path, 'user'), npm_config_foo: 'from-env' },
flatten,
nerfDarts,
npmPath: join(path, 'npm'),
shorthands,
})

await config.load()
await t.rejects(() => config.reload('env'), {
message: 'invalid config location param: env',
})
const originalFlat = config.flat
t.equal(config.getCredentialsByURI(registry).token, 'old-token')
t.equal(config.get('foo'), 'from-env', 'environment config has higher priority')

fs.writeFileSync(userconfig, `${tokenKey}=new-token\nfoo=updated-user\n`)
await config.reload('user')

t.equal(config.getCredentialsByURI(registry).token, 'new-token')
t.equal(config.get('foo'), 'from-env', 'reload preserves higher-priority config')
t.not(config.flat, originalFlat, 'reload invalidates flattened options')
t.equal(config.flat[tokenKey], 'new-token', 'flattened credentials are refreshed')

fs.writeFileSync(userconfig, '')
await config.reload('user')
t.equal(config.getCredentialsByURI(registry).token, undefined, 'removed credentials are cleared')
t.equal(config.get('foo'), 'from-env', 'lower-priority removals do not affect environment config')
})

t.test('setting username/password/email individually', async t => {
const registry = 'https://registry.npmjs.org/'
const path = t.testdir()
Expand Down