mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-08 01:00:51 -04:00
## Summary Rename all internal packages so their npm names follow the `@pnpm/<domain>.<leaf>` convention, matching their directory structure. Also rename directories to remove redundancy and improve clarity. ### Bulk rename (94 packages) All `@pnpm/` packages now derive their name from their directory path using dot-separated segments. Exceptions: `packages/`, `__utils__/`, and `pnpm/artifacts/` keep leaf names only. ### Directory renames (removing redundant prefixes) - `cli/cli-meta` → `cli/meta`, `cli/cli-utils` → `cli/utils` - `config/config` → `config/reader`, `config/config-writer` → `config/writer` - `fetching/fetching-types` → `fetching/types` - `lockfile/lockfile-to-pnp` → `lockfile/to-pnp` - `store/store-connection-manager` → `store/connection-manager` - `store/store-controller-types` → `store/controller-types` - `store/store-path` → `store/path` ### Targeted renames (clarity improvements) - `deps/dependency-path` → `deps/path` (`@pnpm/deps.path`) - `deps/calc-dep-state` → `deps/graph-hasher` (`@pnpm/deps.graph-hasher`) - `deps/inspection/dependencies-hierarchy` → `deps/inspection/tree-builder` (`@pnpm/deps.inspection.tree-builder`) - `bins/link-bins` → `bins/linker`, `bins/remove-bins` → `bins/remover`, `bins/package-bins` → `bins/resolver` - `installing/get-context` → `installing/context` - `store/package-store` → `store/controller` - `pkg-manifest/manifest-utils` → `pkg-manifest/utils` ### Manifest reader/writer renames - `workspace/read-project-manifest` → `workspace/project-manifest-reader` (`@pnpm/workspace.project-manifest-reader`) - `workspace/write-project-manifest` → `workspace/project-manifest-writer` (`@pnpm/workspace.project-manifest-writer`) - `workspace/read-manifest` → `workspace/workspace-manifest-reader` (`@pnpm/workspace.workspace-manifest-reader`) - `workspace/manifest-writer` → `workspace/workspace-manifest-writer` (`@pnpm/workspace.workspace-manifest-writer`) ### Workspace package renames - `workspace/find-packages` → `workspace/projects-reader` - `workspace/find-workspace-dir` → `workspace/root-finder` - `workspace/resolve-workspace-range` → `workspace/range-resolver` - `workspace/filter-packages-from-dir` merged into `workspace/filter-workspace-packages` → `workspace/projects-filter` ### Domain moves - `pkg-manifest/read-project-manifest` → `workspace/project-manifest-reader` - `pkg-manifest/write-project-manifest` → `workspace/project-manifest-writer` - `pkg-manifest/exportable-manifest` → `releasing/exportable-manifest` ### Scope - 1206 files changed - Updated: package.json names/deps, TypeScript imports, tsconfig references, changeset files, renovate.json, test fixtures, import ordering
473 lines
16 KiB
TypeScript
473 lines
16 KiB
TypeScript
import path from 'node:path'
|
|
|
|
import { toOutput$ } from '@pnpm/cli.default-reporter'
|
|
import { PnpmError } from '@pnpm/error'
|
|
import {
|
|
createStreamParser,
|
|
logger,
|
|
} from '@pnpm/logger'
|
|
import chalk from 'chalk'
|
|
import { loadJsonFileSync } from 'load-json-file'
|
|
import normalizeNewline from 'normalize-newline'
|
|
import { firstValueFrom } from 'rxjs'
|
|
import { map, take } from 'rxjs/operators'
|
|
import StackTracey from 'stacktracey'
|
|
|
|
interface Exception extends NodeJS.ErrnoException {
|
|
prefix?: string
|
|
stage?: string
|
|
}
|
|
|
|
const formatErrorCode = (code: string) => chalk.bgRed.black(`\u2009${code}\u2009`)
|
|
const formatError = (code: string, message: string) => {
|
|
return `${formatErrorCode(code)} ${chalk.red(message)}`
|
|
}
|
|
const ERROR_PAD = ''
|
|
|
|
test('prints generic error', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err = new Error('some error')
|
|
logger.error(err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERROR', 'some error')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}${(new StackTracey(err.stack).asTable() as string).split('\n').join(`\n${ERROR_PAD}`)}`)
|
|
})
|
|
|
|
test('prints generic error when recursive install fails', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['recursive'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err: Exception = new Error('some error')
|
|
err['prefix'] = '/home/src/'
|
|
logger.error(err, err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`/home/src/:
|
|
${formatError('ERROR', 'some error')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}${(new StackTracey(err.stack).asTable() as string).split('\n').join(`\n${ERROR_PAD}`)}`)
|
|
})
|
|
|
|
test('prints no matching version error when many dist-tags exist', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err = Object.assign(new PnpmError('NO_MATCHING_VERSION', 'No matching version found for pnpm@1000.0.0'), {
|
|
packageMeta: loadJsonFileSync(path.join(import.meta.dirname, 'pnpm-meta.json')),
|
|
})
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERR_PNPM_NO_MATCHING_VERSION', 'No matching version found for pnpm@1000.0.0')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}The latest release of pnpm is "2.4.0".
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Other releases are:
|
|
${ERROR_PAD} * stable: 2.2.2
|
|
${ERROR_PAD} * next: 2.4.0
|
|
${ERROR_PAD} * latest-1: 1.43.1
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}If you need the full list of all 281 published versions run "pnpm view pnpm versions".`)
|
|
})
|
|
|
|
test('prints no matching version error when only the latest dist-tag exists', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err = Object.assign(new PnpmError('NO_MATCHING_VERSION', 'No matching version found for is-positive@1000.0.0'), {
|
|
packageMeta: loadJsonFileSync(path.join(import.meta.dirname, 'is-positive-meta.json')),
|
|
})
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERR_PNPM_NO_MATCHING_VERSION', 'No matching version found for is-positive@1000.0.0')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}The latest release of is-positive is "3.1.0".
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}If you need the full list of all 4 published versions run "pnpm view is-positive versions".`)
|
|
})
|
|
|
|
test('prints suggestions when an internet-connection related error happens', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err = Object.assign(
|
|
new PnpmError('BAD_TARBALL_SIZE', 'Actual size (99) of tarball (https://foo) did not match the one specified in \'Content-Length\' header (100)'),
|
|
{
|
|
prefix: '/project-dir',
|
|
pkgsStack: [
|
|
{
|
|
id: 'registry.npmjs.org/foo/1.0.0',
|
|
name: 'foo',
|
|
version: '1.0.0',
|
|
},
|
|
],
|
|
expectedSize: 100,
|
|
receivedSize: 99,
|
|
}
|
|
)
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`/project-dir:
|
|
${formatError('ERR_PNPM_BAD_TARBALL_SIZE', 'Actual size (99) of tarball (https://foo) did not match the one specified in \'Content-Length\' header (100)')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}This error happened while installing the dependencies of foo@1.0.0
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Seems like you have internet connection issues.
|
|
${ERROR_PAD}Try running the same command again.
|
|
${ERROR_PAD}If that doesn't help, try one of the following:
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}- Set a bigger value for the \`fetch-retries\` config.
|
|
${ERROR_PAD} To check the current value of \`fetch-retries\`, run \`pnpm get fetch-retries\`.
|
|
${ERROR_PAD} To set a new value, run \`pnpm set fetch-retries <number>\`.
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}- Set \`network-concurrency\` to 1.
|
|
${ERROR_PAD} This change will slow down installation times, so it is recommended to
|
|
${ERROR_PAD} delete the config once the internet connection is good again: \`pnpm config delete network-concurrency\`
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}NOTE: You may also override configs via flags.
|
|
${ERROR_PAD}For instance, \`pnpm install --fetch-retries 5 --network-concurrency 1\``)
|
|
})
|
|
|
|
test('prints test error', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['run', 'test'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err = Object.assign(new Error('Tests failed'), {
|
|
code: 'ELIFECYCLE',
|
|
stage: 'test',
|
|
})
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ELIFECYCLE', 'Test failed. See above for more details.')}`)
|
|
})
|
|
|
|
test('prints command error with exit code', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['run', 'lint'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err: Exception = new Error('Command failed')
|
|
err['errno'] = 100
|
|
err['stage'] = 'lint'
|
|
err['code'] = 'ELIFECYCLE'
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ELIFECYCLE', 'Command failed with exit code 100.')}`)
|
|
})
|
|
|
|
test('prints command error without exit code', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['run', 'lint'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err: Exception = new Error('Command failed')
|
|
err['stage'] = 'lint'
|
|
err['code'] = 'ELIFECYCLE'
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ELIFECYCLE', 'Command failed.')}`)
|
|
})
|
|
|
|
test('prints unsupported pnpm version error', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err = Object.assign(new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version'), {
|
|
packageId: '/home/zoltan/project',
|
|
wanted: { pnpm: '2' },
|
|
current: { pnpm: '3.0.0', node: '10.0.0' },
|
|
})
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERR_PNPM_UNSUPPORTED_ENGINE', 'Unsupported environment (bad pnpm and/or Node.js version)')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Your pnpm version is incompatible with "/home/zoltan/project".
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Expected version: 2
|
|
${ERROR_PAD}Got: 3.0.0
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}This is happening because the package's manifest has an engines.pnpm field specified.
|
|
${ERROR_PAD}To fix this issue, install the required pnpm version globally.
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}To install the latest version of pnpm, run "pnpm i -g pnpm".
|
|
${ERROR_PAD}To check your pnpm version, run "pnpm -v".`)
|
|
})
|
|
|
|
test('prints unsupported Node version error', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err = Object.assign(new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version'), {
|
|
packageId: '/home/zoltan/project',
|
|
wanted: { node: '>=12' },
|
|
current: { pnpm: '3.0.0', node: '10.0.0' },
|
|
})
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERR_PNPM_UNSUPPORTED_ENGINE', 'Unsupported environment (bad pnpm and/or Node.js version)')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Your Node version is incompatible with "/home/zoltan/project".
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Expected version: >=12
|
|
${ERROR_PAD}Got: 10.0.0
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}This is happening because the package's manifest has an engines.node field specified.
|
|
${ERROR_PAD}To fix this issue, install the required Node version.`)
|
|
})
|
|
|
|
test('prints unsupported pnpm and Node versions error', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
expect.assertions(1)
|
|
|
|
const err = Object.assign(new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version'), {
|
|
packageId: '/home/zoltan/project',
|
|
wanted: { pnpm: '2', node: '>=12' },
|
|
current: { pnpm: '3.0.0', node: '10.0.0' },
|
|
})
|
|
logger.error(err, err)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERR_PNPM_UNSUPPORTED_ENGINE', 'Unsupported environment (bad pnpm and/or Node.js version)')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Your pnpm version is incompatible with "/home/zoltan/project".
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Expected version: 2
|
|
${ERROR_PAD}Got: 3.0.0
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}This is happening because the package's manifest has an engines.pnpm field specified.
|
|
${ERROR_PAD}To fix this issue, install the required pnpm version globally.
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}To install the latest version of pnpm, run "pnpm i -g pnpm".
|
|
${ERROR_PAD}To check your pnpm version, run "pnpm -v".` + '\n\n' + `\
|
|
${ERROR_PAD}Your Node version is incompatible with "/home/zoltan/project".
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}Expected version: >=12
|
|
${ERROR_PAD}Got: 10.0.0
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}This is happening because the package's manifest has an engines.node field specified.
|
|
${ERROR_PAD}To fix this issue, install the required Node version.`)
|
|
})
|
|
|
|
test('prints error even if the error object not passed in through the message object', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err = new PnpmError('SOME_ERROR', 'some error')
|
|
logger.error(err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(formatError('ERR_PNPM_SOME_ERROR', 'some error'))
|
|
})
|
|
|
|
test('prints error without packages stacktrace when pkgsStack is empty but do print the project directory path', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err = new PnpmError('SOME_ERROR', 'some error')
|
|
err.prefix = '/project-dir'
|
|
err.pkgsStack = []
|
|
logger.error(err, err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`/project-dir:
|
|
${formatError('ERR_PNPM_SOME_ERROR', 'some error')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}This error happened while installing a direct dependency of /project-dir`)
|
|
})
|
|
|
|
test('prints error with packages stacktrace - depth 1 and hint', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err = new PnpmError('SOME_ERROR', 'some error', { hint: 'hint' })
|
|
err.pkgsStack = [
|
|
{
|
|
id: 'registry.npmjs.org/foo/1.0.0',
|
|
name: 'foo',
|
|
version: '1.0.0',
|
|
},
|
|
]
|
|
logger.error(err, err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERR_PNPM_SOME_ERROR', 'some error')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}This error happened while installing the dependencies of foo@1.0.0
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}hint`)
|
|
})
|
|
|
|
test('prints error with packages stacktrace - depth 2', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err = new PnpmError('SOME_ERROR', 'some error')
|
|
err.prefix = '/project-dir'
|
|
err.pkgsStack = [
|
|
{
|
|
id: 'registry.npmjs.org/foo/1.0.0',
|
|
name: 'foo',
|
|
version: '1.0.0',
|
|
},
|
|
{
|
|
id: 'registry.npmjs.org/bar/1.0.0',
|
|
name: 'bar',
|
|
version: '1.0.0',
|
|
},
|
|
]
|
|
logger.error(err, err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`/project-dir:
|
|
${formatError('ERR_PNPM_SOME_ERROR', 'some error')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}This error happened while installing the dependencies of foo@1.0.0
|
|
${ERROR_PAD} at bar@1.0.0`)
|
|
})
|
|
|
|
test('prints error and hint', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err = new PnpmError('SOME_ERROR', 'some error', { hint: 'some hint' })
|
|
logger.error(err, err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(formatErrorCode('ERR_PNPM_SOME_ERROR') + ' ' + `${chalk.red('some error')}
|
|
|
|
some hint`)
|
|
})
|
|
|
|
test('prints authorization error with auth settings', async () => {
|
|
const rawConfig = {
|
|
'//foo.bar:_auth': '9876543219',
|
|
'//foo.bar:_authToken': '9876543219',
|
|
'//foo.bar:_password': '9876543219',
|
|
'//foo.bar:username': 'kiss.reka',
|
|
'@foo:registry': 'https://foo.bar',
|
|
_auth: '0123456789',
|
|
_authToken: '0123456789',
|
|
_password: '0123456789',
|
|
username: 'nagy.gabor',
|
|
}
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'], config: { rawConfig } as any }, // eslint-disable-line
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err = new PnpmError('FETCH_401', 'some error', { hint: 'some hint' })
|
|
logger.error(err, err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERR_PNPM_FETCH_401', 'some error')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}some hint
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}These authorization settings were found:
|
|
${ERROR_PAD}//foo.bar:_auth=9876[hidden]
|
|
${ERROR_PAD}//foo.bar:_authToken=9876[hidden]
|
|
${ERROR_PAD}//foo.bar:_password=[hidden]
|
|
${ERROR_PAD}//foo.bar:username=kiss.reka
|
|
${ERROR_PAD}@foo:registry=https://foo.bar
|
|
${ERROR_PAD}_auth=0123[hidden]
|
|
${ERROR_PAD}_authToken=0123[hidden]
|
|
${ERROR_PAD}_password=[hidden]
|
|
${ERROR_PAD}username=nagy.gabor`)
|
|
})
|
|
|
|
test('prints authorization error without auth settings, where there are none', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'], config: { rawConfig: {} } as any }, // eslint-disable-line
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const err = new PnpmError('FETCH_401', 'some error', { hint: 'some hint' })
|
|
logger.error(err, err)
|
|
|
|
expect.assertions(1)
|
|
|
|
const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline)))
|
|
expect(output).toBe(`${formatError('ERR_PNPM_FETCH_401', 'some error')}
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}some hint
|
|
${ERROR_PAD}
|
|
${ERROR_PAD}No authorization settings were found in the configs.
|
|
${ERROR_PAD}Try to log in to the registry by running "pnpm login"
|
|
${ERROR_PAD}or add the auth tokens manually to the ~/.npmrc file.`)
|
|
})
|