mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-10 18:18:56 -04:00
feat: rename pnpmfile.js to .pnpmfile.cjs (#3149)
close #3145 close #1339
This commit is contained in:
6
.changeset/long-dancers-allow.md
Normal file
6
.changeset/long-dancers-allow.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": major
|
||||
"@pnpm/pnpmfile": major
|
||||
---
|
||||
|
||||
`pnpmfile.js` renamed to `.pnpmfile.cjs`.
|
||||
@@ -858,7 +858,7 @@ test('install: print hook message', (done) => {
|
||||
})
|
||||
|
||||
hookLogger.debug({
|
||||
from: '/home/jane/repo/pnpmfile.js',
|
||||
from: '/home/jane/repo/.pnpmfile.cjs',
|
||||
hook: 'readPackage',
|
||||
message: 'foo',
|
||||
prefix: '/home/jane/repo',
|
||||
@@ -885,7 +885,7 @@ test('recursive: print hook message', (done) => {
|
||||
})
|
||||
|
||||
hookLogger.debug({
|
||||
from: '/home/jane/repo/pnpmfile.js',
|
||||
from: '/home/jane/repo/.pnpmfile.cjs',
|
||||
hook: 'readPackage',
|
||||
message: 'foo',
|
||||
prefix: '/home/jane/repo/pkg-1',
|
||||
|
||||
@@ -160,7 +160,7 @@ by any dependencies, so it is an emulation of a flat node_modules',
|
||||
name: '--child-concurrency <number>',
|
||||
},
|
||||
{
|
||||
description: 'Disable pnpm hooks defined in pnpmfile.js',
|
||||
description: 'Disable pnpm hooks defined in .pnpmfile.cjs',
|
||||
name: '--ignore-pnpmfile',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ const DEFAULT_OPTIONS = {
|
||||
optionalDependencies: true,
|
||||
},
|
||||
lock: true,
|
||||
pnpmfile: 'pnpmfile.js',
|
||||
pnpmfile: '.pnpmfile.cjs',
|
||||
rawConfig: { registry: REGISTRY_URL },
|
||||
rawLocalConfig: { registry: REGISTRY_URL },
|
||||
registries: {
|
||||
|
||||
@@ -17,7 +17,7 @@ const DEFAULT_OPTIONS = {
|
||||
optionalDependencies: true,
|
||||
},
|
||||
lock: true,
|
||||
pnpmfile: 'pnpmfile.js',
|
||||
pnpmfile: '.pnpmfile.cjs',
|
||||
rawConfig: { registry: REGISTRY_URL },
|
||||
rawLocalConfig: { registry: REGISTRY_URL },
|
||||
registries: {
|
||||
|
||||
@@ -18,7 +18,7 @@ const DEFAULT_OPTIONS = {
|
||||
optionalDependencies: true,
|
||||
},
|
||||
lock: true,
|
||||
pnpmfile: 'pnpmfile.js',
|
||||
pnpmfile: '.pnpmfile.cjs',
|
||||
rawConfig: { registry: REGISTRY_URL },
|
||||
rawLocalConfig: { registry: REGISTRY_URL },
|
||||
registries: {
|
||||
|
||||
@@ -31,7 +31,7 @@ const DEFAULT_OPTIONS = {
|
||||
optionalDependencies: true,
|
||||
},
|
||||
lock: true,
|
||||
pnpmfile: 'pnpmfile.js',
|
||||
pnpmfile: '.pnpmfile.cjs',
|
||||
rawConfig: { registry: REGISTRY_URL },
|
||||
rawLocalConfig: { registry: REGISTRY_URL },
|
||||
registries: {
|
||||
|
||||
@@ -30,7 +30,7 @@ export const DEFAULT_OPTS = {
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
pnpmfile: './pnpmfile.js',
|
||||
pnpmfile: './.pnpmfile.cjs',
|
||||
proxy: undefined,
|
||||
rawConfig: { registry: REGISTRY },
|
||||
rawLocalConfig: {},
|
||||
|
||||
@@ -30,7 +30,7 @@ export const DEFAULT_OPTS = {
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
pnpmfile: './pnpmfile.js',
|
||||
pnpmfile: './.pnpmfile.cjs',
|
||||
proxy: undefined,
|
||||
rawConfig: { registry: REGISTRY },
|
||||
rawLocalConfig: {},
|
||||
|
||||
@@ -31,7 +31,7 @@ export const DEFAULT_OPTS = {
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
pnpmfile: './pnpmfile.js',
|
||||
pnpmfile: './.pnpmfile.cjs',
|
||||
proxy: undefined,
|
||||
rawConfig: { registry: REGISTRY },
|
||||
rawLocalConfig: {},
|
||||
|
||||
@@ -30,7 +30,7 @@ export const DEFAULT_OPTS = {
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
pnpmfile: './pnpmfile.js',
|
||||
pnpmfile: './.pnpmfile.cjs',
|
||||
proxy: undefined,
|
||||
rawConfig: { registry: REGISTRY },
|
||||
rawLocalConfig: {},
|
||||
|
||||
@@ -30,7 +30,7 @@ export const DEFAULT_OPTS = {
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
pnpmfile: './pnpmfile.js',
|
||||
pnpmfile: './.pnpmfile.cjs',
|
||||
proxy: undefined,
|
||||
rawConfig: { registry: REGISTRY },
|
||||
rawLocalConfig: {},
|
||||
|
||||
@@ -28,7 +28,7 @@ export const DEFAULT_OPTS = {
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
pnpmfile: './pnpmfile.js',
|
||||
pnpmfile: './.pnpmfile.cjs',
|
||||
proxy: undefined,
|
||||
rawConfig: { registry: REGISTRY },
|
||||
rawLocalConfig: {},
|
||||
|
||||
@@ -18,7 +18,7 @@ test('readPackage hook in single project doesn\'t modify manifest', async () =>
|
||||
return pkg
|
||||
}
|
||||
`
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
await execPnpm(['add', 'is-positive@1.0.0'])
|
||||
let pkg: PackageManifest = await loadJsonFile(path.resolve('package.json'))
|
||||
expect(pkg?.dependencies).toStrictEqual({ 'is-positive': '1.0.0' }) // add dependency & readPackage hook work
|
||||
@@ -59,7 +59,7 @@ test('readPackage hook in monorepo doesn\'t modify manifest', async () => {
|
||||
return pkg
|
||||
}
|
||||
`
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
await execPnpm(['add', 'is-positive@1.0.0', '--filter', 'project-a'])
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
test('readPackage hook', async () => {
|
||||
const project = prepare()
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -38,7 +38,7 @@ test('readPackage hook', async () => {
|
||||
test('readPackage hook makes installation fail if it does not return the modified package manifests', async () => {
|
||||
prepare()
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -80,7 +80,7 @@ test('readPackage hook from custom location', async () => {
|
||||
test('readPackage hook from global pnpmfile', async () => {
|
||||
const project = prepare()
|
||||
|
||||
await fs.writeFile('../pnpmfile.js', `
|
||||
await fs.writeFile('../.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -97,7 +97,7 @@ test('readPackage hook from global pnpmfile', async () => {
|
||||
// w/o the hook, 100.1.0 would be installed
|
||||
await addDistTag('dep-of-pkg-with-1-dep', '100.1.0', 'latest')
|
||||
|
||||
await execPnpm(['install', 'pkg-with-1-dep', '--global-pnpmfile', path.resolve('..', 'pnpmfile.js')])
|
||||
await execPnpm(['install', 'pkg-with-1-dep', '--global-pnpmfile', path.resolve('..', '.pnpmfile.cjs')])
|
||||
|
||||
await project.storeHas('dep-of-pkg-with-1-dep', '100.0.0')
|
||||
})
|
||||
@@ -105,7 +105,7 @@ test('readPackage hook from global pnpmfile', async () => {
|
||||
test('readPackage hook from global pnpmfile and local pnpmfile', async () => {
|
||||
const project = prepare()
|
||||
|
||||
await fs.writeFile('../pnpmfile.js', `
|
||||
await fs.writeFile('../.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -120,7 +120,7 @@ test('readPackage hook from global pnpmfile and local pnpmfile', async () => {
|
||||
}
|
||||
`, 'utf8')
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -137,7 +137,7 @@ test('readPackage hook from global pnpmfile and local pnpmfile', async () => {
|
||||
// w/o the hook, 100.1.0 would be installed
|
||||
await addDistTag('dep-of-pkg-with-1-dep', '100.1.0', 'latest')
|
||||
|
||||
await execPnpm(['install', 'pkg-with-1-dep', '--global-pnpmfile', path.resolve('..', 'pnpmfile.js')])
|
||||
await execPnpm(['install', 'pkg-with-1-dep', '--global-pnpmfile', path.resolve('..', '.pnpmfile.cjs')])
|
||||
|
||||
await project.storeHas('dep-of-pkg-with-1-dep', '100.0.0')
|
||||
await project.storeHas('is-positive', '1.0.0')
|
||||
@@ -163,7 +163,7 @@ test('readPackage hook from pnpmfile at root of workspace', async () => {
|
||||
return pkg
|
||||
}
|
||||
`
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
|
||||
await writeYamlFile('pnpm-workspace.yaml', { packages: ['project-1'] })
|
||||
|
||||
@@ -198,7 +198,7 @@ test('readPackage hook during update', async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -220,10 +220,10 @@ test('readPackage hook during update', async () => {
|
||||
await project.storeHas('dep-of-pkg-with-1-dep', '100.0.0')
|
||||
})
|
||||
|
||||
test('prints meaningful error when there is syntax error in pnpmfile.js', async () => {
|
||||
test('prints meaningful error when there is syntax error in .pnpmfile.cjs', async () => {
|
||||
prepare()
|
||||
|
||||
await fs.writeFile('pnpmfile.js', '/boom', 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', '/boom', 'utf8')
|
||||
|
||||
const proc = execPnpmSync(['install', 'pkg-with-1-dep'])
|
||||
|
||||
@@ -231,10 +231,10 @@ test('prints meaningful error when there is syntax error in pnpmfile.js', async
|
||||
expect(proc.status).toBe(1)
|
||||
})
|
||||
|
||||
test('fails when pnpmfile.js requires a non-existend module', async () => {
|
||||
test('fails when .pnpmfile.cjs requires a non-existend module', async () => {
|
||||
prepare()
|
||||
|
||||
await fs.writeFile('pnpmfile.js', 'module.exports = require("./this-does-node-exist")', 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', 'module.exports = require("./this-does-node-exist")', 'utf8')
|
||||
|
||||
const proc = execPnpmSync(['install', 'pkg-with-1-dep'])
|
||||
|
||||
@@ -242,10 +242,10 @@ test('fails when pnpmfile.js requires a non-existend module', async () => {
|
||||
expect(proc.status).toBe(1)
|
||||
})
|
||||
|
||||
test('ignore pnpmfile.js when --ignore-pnpmfile is used', async () => {
|
||||
test('ignore .pnpmfile.cjs when --ignore-pnpmfile is used', async () => {
|
||||
const project = prepare()
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -266,14 +266,14 @@ test('ignore pnpmfile.js when --ignore-pnpmfile is used', async () => {
|
||||
await project.storeHas('dep-of-pkg-with-1-dep', '100.1.0')
|
||||
})
|
||||
|
||||
test('ignore pnpmfile.js during update when --ignore-pnpmfile is used', async () => {
|
||||
test('ignore .pnpmfile.cjs during update when --ignore-pnpmfile is used', async () => {
|
||||
const project = prepare({
|
||||
dependencies: {
|
||||
'pkg-with-1-dep': '*',
|
||||
},
|
||||
})
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -297,7 +297,7 @@ test('ignore pnpmfile.js during update when --ignore-pnpmfile is used', async ()
|
||||
test('pnpmfile: pass log function to readPackage hook', async () => {
|
||||
const project = prepare()
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -335,7 +335,7 @@ test('pnpmfile: pass log function to readPackage hook', async () => {
|
||||
test('pnpmfile: pass log function to readPackage hook of global and local pnpmfile', async () => {
|
||||
const project = prepare()
|
||||
|
||||
await fs.writeFile('../pnpmfile.js', `
|
||||
await fs.writeFile('../.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -351,7 +351,7 @@ test('pnpmfile: pass log function to readPackage hook of global and local pnpmfi
|
||||
}
|
||||
`, 'utf8')
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -369,7 +369,7 @@ test('pnpmfile: pass log function to readPackage hook of global and local pnpmfi
|
||||
// w/o the hook, 100.1.0 would be installed
|
||||
await addDistTag('dep-of-pkg-with-1-dep', '100.1.0', 'latest')
|
||||
|
||||
const proc = execPnpmSync(['install', 'pkg-with-1-dep', '--global-pnpmfile', path.resolve('..', 'pnpmfile.js'), '--reporter', 'ndjson'])
|
||||
const proc = execPnpmSync(['install', 'pkg-with-1-dep', '--global-pnpmfile', path.resolve('..', '.pnpmfile.cjs'), '--reporter', 'ndjson'])
|
||||
|
||||
await project.storeHas('dep-of-pkg-with-1-dep', '100.0.0')
|
||||
await project.storeHas('is-positive', '1.0.0')
|
||||
@@ -399,7 +399,7 @@ test('pnpmfile: pass log function to readPackage hook of global and local pnpmfi
|
||||
test('pnpmfile: run afterAllResolved hook', async () => {
|
||||
prepare()
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -429,7 +429,7 @@ test('pnpmfile: run afterAllResolved hook', async () => {
|
||||
test('readPackage hook normalizes the package manifest', async () => {
|
||||
prepare()
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -454,7 +454,7 @@ test('readPackage hook overrides project package', async () => {
|
||||
name: 'test-read-package-hook',
|
||||
})
|
||||
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
@@ -494,7 +494,7 @@ test('readPackage hook is used during removal inside a workspace', async () => {
|
||||
])
|
||||
|
||||
await writeYamlFile('pnpm-workspace.yaml', { packages: ['project-1'] })
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
'use strict'
|
||||
module.exports = {
|
||||
hooks: {
|
||||
|
||||
@@ -716,7 +716,7 @@ test('recursive installation with shared-workspace-lockfile and a readPackage ho
|
||||
return pkg
|
||||
}
|
||||
`
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
await execPnpm(['recursive', 'install', '--shared-workspace-lockfile', '--store-dir', 'store'])
|
||||
|
||||
@@ -166,10 +166,10 @@ test('recursive installation of packages with hooks', async () => {
|
||||
return pkg
|
||||
}
|
||||
`
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
|
||||
process.chdir('../project-2')
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
|
||||
process.chdir('..')
|
||||
|
||||
@@ -213,13 +213,13 @@ test('recursive installation of packages in workspace ignores hooks in packages'
|
||||
return pkg
|
||||
}
|
||||
`
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
|
||||
process.chdir('../project-2')
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
|
||||
process.chdir('..')
|
||||
await fs.writeFile('pnpmfile.js', `
|
||||
await fs.writeFile('.pnpmfile.cjs', `
|
||||
module.exports = { hooks: { readPackage } }
|
||||
function readPackage (pkg) {
|
||||
pkg.dependencies = pkg.dependencies || {}
|
||||
@@ -239,7 +239,7 @@ test('recursive installation of packages in workspace ignores hooks in packages'
|
||||
/* eslint-enable @typescript-eslint/no-unnecessary-type-assertion */
|
||||
})
|
||||
|
||||
test('ignores pnpmfile.js during recursive installation when --ignore-pnpmfile is used', async () => {
|
||||
test('ignores .pnpmfile.cjs during recursive installation when --ignore-pnpmfile is used', async () => {
|
||||
// This test hangs on Appveyor for some reason
|
||||
if (isCI && isWindows()) return
|
||||
const projects = preparePackages([
|
||||
@@ -270,10 +270,10 @@ test('ignores pnpmfile.js during recursive installation when --ignore-pnpmfile i
|
||||
return pkg
|
||||
}
|
||||
`
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
|
||||
process.chdir('../project-2')
|
||||
await fs.writeFile('pnpmfile.js', pnpmfile, 'utf8')
|
||||
await fs.writeFile('.pnpmfile.cjs', pnpmfile, 'utf8')
|
||||
|
||||
process.chdir('..')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# @pnpm/pnpmfile
|
||||
|
||||
> Reading a pnpmfile.js
|
||||
> Reading a .pnpmfile.cjs
|
||||
|
||||
[](https://www.npmjs.com/package/@pnpm/pnpmfile)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@pnpm/pnpmfile",
|
||||
"version": "0.1.21",
|
||||
"description": "Reading a pnpmfile.js",
|
||||
"description": "Reading a .pnpmfile.cjs",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function requireHooks (
|
||||
let globalHooks = globalPnpmfile?.hooks
|
||||
|
||||
const pnpmFile = opts.pnpmfile && requirePnpmfile(pathAbsolute(opts.pnpmfile, prefix), prefix) ||
|
||||
requirePnpmfile(path.join(prefix, 'pnpmfile.js'), prefix)
|
||||
requirePnpmfile(path.join(prefix, '.pnpmfile.cjs'), prefix)
|
||||
let hooks = pnpmFile?.hooks
|
||||
|
||||
if (!globalHooks && !hooks) return {}
|
||||
|
||||
@@ -65,7 +65,7 @@ export default (pnpmFilePath: string, prefix: string) => {
|
||||
return pnpmfile
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
console.error(chalk.red('A syntax error in the pnpmfile.js\n'))
|
||||
console.error(chalk.red('A syntax error in the .pnpmfile.cjs\n'))
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -77,8 +77,8 @@ export default (pnpmFilePath: string, prefix: string) => {
|
||||
}
|
||||
|
||||
function pnpmFileExistsSync (pnpmFilePath: string) {
|
||||
const pnpmFileRealName = pnpmFilePath.endsWith('.js')
|
||||
const pnpmFileRealName = pnpmFilePath.endsWith('.cjs')
|
||||
? pnpmFilePath
|
||||
: `${pnpmFilePath}.js`
|
||||
: `${pnpmFilePath}.cjs`
|
||||
return fs.existsSync(pnpmFileRealName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user