Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/all-tables-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@pnpm/config": minor
"pnpm": minor
---

Allow loading certificates from `cert`, `ca`, and `key` for specific registry URLs. E.g., `//registry.example.com/:ca=-----BEGIN CERTIFICATE-----...`. Previously this was only working via `certfile`, `cafile`, and `keyfile`.

These properties are supported in `.npmrc`, but were ignored by pnpm, this will make pnpm read and use them as well.

Related PR: [#10230](https://github.com/pnpm/pnpm/pull/10230).
6 changes: 6 additions & 0 deletions .changeset/brave-ties-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-init": minor
"pnpm": minor
---

Added a new flag called `--bare` to `pnpm init` for creating a package.json with the bare minimum of required fields [#10226](https://github.com/pnpm/pnpm/issues/10226).
3 changes: 3 additions & 0 deletions config/config/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ const RAW_AUTH_CFG_KEYS = [
] satisfies Array<keyof typeof types>

const RAW_AUTH_CFG_KEY_SUFFIXES = [
':ca',
':cafile',
':cert',
':certfile',
':key',
':keyfile',
':registry',
':tokenHelper',
Expand Down
46 changes: 32 additions & 14 deletions config/config/src/getNetworkConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,47 @@ export interface GetNetworkConfigsResult {
}

export function getNetworkConfigs (rawConfig: Record<string, object>): GetNetworkConfigsResult {
// Get all the auth options that have :certfile or :keyfile in their name
// Get all the auth options that have SSL certificate data or file references
const sslConfigs: Record<string, SslConfig> = {}
const registries: Record<string, string> = {}
for (const [configKey, value] of Object.entries(rawConfig)) {
if (configKey[0] === '@' && configKey.endsWith(':registry')) {
registries[configKey.slice(0, configKey.indexOf(':'))] = normalizeRegistryUrl(value as unknown as string)
} else if (configKey.includes(':certfile') || configKey.includes(':keyfile') || configKey.includes(':cafile')) {
// Split by '/:' because the registry may contain a port
const registry = configKey.split('/:')[0] + '/'
if (!sslConfigs[registry]) {
sslConfigs[registry] = { cert: '', key: '' }
}
if (configKey.includes(':certfile')) {
sslConfigs[registry].cert = fs.readFileSync(value as unknown as string, 'utf8')
} else if (configKey.includes(':keyfile')) {
sslConfigs[registry].key = fs.readFileSync(value as unknown as string, 'utf8')
} else if (configKey.includes(':cafile')) {
sslConfigs[registry].ca = fs.readFileSync(value as unknown as string, 'utf8')
}
continue
}

const parsed = tryParseSslSetting(configKey)
if (!parsed) continue

const { registry, sslConfigKey, isFile } = parsed
if (!sslConfigs[registry]) {
sslConfigs[registry] = { cert: '', key: '' }
}
sslConfigs[registry][sslConfigKey] = isFile
? fs.readFileSync(value as unknown as string, 'utf8')
: (value as unknown as string).replace(/\\n/g, '\n')
}
return {
registries,
sslConfigs,
}
}

const SSL_SUFFIX_RE = /:(?<id>cert|key|ca)(?<kind>file)?$/

interface ParsedSslSetting {
registry: string
sslConfigKey: keyof SslConfig
isFile: boolean
}

function tryParseSslSetting (key: string): ParsedSslSetting | null {
const match = key.match(SSL_SUFFIX_RE)
if (!match?.groups) {
return null
}
const registry = key.slice(0, match.index!) // already includes the trailing slash
const sslConfigKey = match.groups.id as keyof SslConfig
const isFile = Boolean(match.groups.kind)
return { registry, sslConfigKey, isFile }
}
32 changes: 32 additions & 0 deletions config/config/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,38 @@ test('getConfig() should read cafile', async () => {
-----END CERTIFICATE-----`])
})

test('getConfig() should read inline SSL certificates from .npmrc', async () => {
prepareEmpty()

// These are written to .npmrc with literal \n strings
const inlineCa = '-----BEGIN CERTIFICATE-----\\nMIIFNzCCAx+gAwIBAgIQNB613yRzpKtDztlXiHmOGDANBgkqhkiG9w0BAQsFADAR\\n-----END CERTIFICATE-----'
const inlineCert = '-----BEGIN CERTIFICATE-----\\nMIIClientCert\\n-----END CERTIFICATE-----'
const inlineKey = '-----BEGIN PRIVATE KEY-----\\nMIIClientKey\\n-----END PRIVATE KEY-----'

const npmrc = [
'//registry.example.com/:ca=' + inlineCa,
'//registry.example.com/:cert=' + inlineCert,
'//registry.example.com/:key=' + inlineKey,
].join('\n')
fs.writeFileSync('.npmrc', npmrc, 'utf8')

const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})

// After processing, \n should be converted to actual newlines
expect(config.sslConfigs).toBeDefined()
expect(config.sslConfigs['//registry.example.com/']).toStrictEqual({
ca: inlineCa.replace(/\\n/g, '\n'),
cert: inlineCert.replace(/\\n/g, '\n'),
key: inlineKey.replace(/\\n/g, '\n'),
})
})

test('respect mergeGitBranchLockfilesBranchPattern', async () => {
{
prepareEmpty()
Expand Down
54 changes: 37 additions & 17 deletions packages/plugin-commands-init/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import { parseRawConfig } from './utils.js'
export const rcOptionsTypes = cliOptionsTypes

export function cliOptionsTypes (): Record<string, unknown> {
return pick(['init-type', 'init-package-manager'], allTypes)
return {
...pick([
'init-package-manager',
'init-type',
], allTypes),
bare: Boolean,
}
}

export const commandNames = ['init']
Expand All @@ -34,6 +40,10 @@ export function help (): string {
description: 'Pin the project to the current pnpm version by adding a "packageManager" field to package.json',
name: '--init-package-manager',
},
{
description: 'Create a package.json file with the bare minimum of required fields',
name: '--bare',
},
],
},
],
Expand All @@ -42,10 +52,17 @@ export function help (): string {
})
}

export async function handler (
opts: Pick<UniversalOptions, 'rawConfig'> & Pick<Config, 'cliOptions'> & Partial<Pick<Config, 'initPackageManager' | 'initType'>>,
params?: string[]
): Promise<string> {
export type InitOptions =
& Pick<UniversalOptions, 'rawConfig'>
& Pick<Config, 'cliOptions'>
& Partial<Pick<Config,
| 'initPackageManager'
| 'initType'
>> & {
bare?: boolean
}

export async function handler (opts: InitOptions, params?: string[]): Promise<string> {
if (params?.length) {
throw new PnpmError('INIT_ARG', 'init command does not accept any arguments', {
hint: `Maybe you wanted to run "pnpm create ${params.join(' ')}"`,
Expand All @@ -58,18 +75,20 @@ export async function handler (
if (fs.existsSync(manifestPath)) {
throw new PnpmError('PACKAGE_JSON_EXISTS', 'package.json already exists')
}
const manifest: ProjectManifest = {
name: path.basename(process.cwd()),
version: '1.0.0',
description: '',
main: 'index.js',
scripts: {
test: 'echo "Error: no test specified" && exit 1',
},
keywords: [],
author: '',
license: 'ISC',
}
const manifest: ProjectManifest = opts.bare
? {}
: {
name: path.basename(process.cwd()),
version: '1.0.0',
description: '',
main: 'index.js',
scripts: {
test: 'echo "Error: no test specified" && exit 1',
},
keywords: [],
author: '',
license: 'ISC',
}

if (opts.initType === 'module') {
manifest.type = opts.initType
Expand All @@ -83,6 +102,7 @@ export async function handler (
const priority = Object.fromEntries([
'name',
'version',
'private',
'description',
'main',
'scripts',
Expand Down
13 changes: 13 additions & 0 deletions packages/plugin-commands-init/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,16 @@ test('init a new package.json with init-type=module', async () => {
const manifest = loadJsonFileSync<ProjectManifest>(path.resolve('package.json'))
expect(manifest.type).toBe('module')
})

test('init a new package.json with --bare', async () => {
prepareEmpty()
await init.handler({ rawConfig: {}, cliOptions: {}, bare: true })
const manifest = loadJsonFileSync<ProjectManifest>(path.resolve('package.json'))
expect(manifest).not.toHaveProperty(['name'])
expect(manifest).not.toHaveProperty(['version'])
expect(manifest).not.toHaveProperty(['description'])
expect(manifest).not.toHaveProperty(['main'])
expect(manifest).not.toHaveProperty(['keywords'])
expect(manifest).not.toHaveProperty(['author'])
expect(manifest).not.toHaveProperty(['license'])
})
Loading