diff --git a/packages/next/src/cli/next-info.ts b/packages/next/src/cli/next-info.ts index b04a51370cfc..a79ab66d6b73 100755 --- a/packages/next/src/cli/next-info.ts +++ b/packages/next/src/cli/next-info.ts @@ -102,8 +102,11 @@ async function printInfo() { let versionInfo try { - const registry = getRegistry() - const res = await fetch(`${registry}-/package/next/dist-tags`) + const { registry, authToken } = getRegistry() + const headers: HeadersInit = authToken + ? { Authorization: `Bearer ${authToken}` } + : {} + const res = await fetch(`${registry}-/package/next/dist-tags`, { headers }) const tags = await res.json() versionInfo = parseVersionInfo({ diff --git a/packages/next/src/lib/download-swc.ts b/packages/next/src/lib/download-swc.ts index d42d73832f01..ca253abf2aab 100644 --- a/packages/next/src/lib/download-swc.ts +++ b/packages/next/src/lib/download-swc.ts @@ -34,11 +34,15 @@ async function extractBinary( `${tarFileName}.temp-${Date.now()}` ) - const registry = getRegistry() + const { registry, authToken } = getRegistry() const downloadUrl = `${registry}${pkgName}/-/${tarFileName}` - await fetch(downloadUrl).then((res) => { + const headers: HeadersInit = authToken + ? { Authorization: `Bearer ${authToken}` } + : {} + + await fetch(downloadUrl, { headers }).then((res) => { const { ok, body } = res if (!ok || !body) { Log.error(`Failed to download swc package from ${downloadUrl}`) diff --git a/packages/next/src/lib/helpers/get-registry.ts b/packages/next/src/lib/helpers/get-registry.ts index 629e9b0d265b..6c039efa2037 100644 --- a/packages/next/src/lib/helpers/get-registry.ts +++ b/packages/next/src/lib/helpers/get-registry.ts @@ -7,7 +7,10 @@ import { getFormattedNodeOptionsWithoutInspect } from '../../server/lib/utils' * The URL will have a trailing slash. * @default https://registry.npmjs.org/ */ -export function getRegistry(baseDir: string = process.cwd()) { +export function getRegistry(baseDir: string = process.cwd()): { + registry: string + authToken?: string +} { const pkgManager = getPkgManager(baseDir) // Since `npm config` command fails in npm workspace to prevent workspace config conflicts, // add `--no-workspaces` flag to run under the context of the root project only. @@ -39,5 +42,31 @@ export function getRegistry(baseDir: string = process.cwd()) { }) } - return registry + // Read auth token for this registry from .npmrc, if configured. + let authToken: string | undefined + try { + const parsed = new URL(registry) + let scope = `//${parsed.host}${parsed.pathname}` + if (!scope.endsWith('/')) scope += '/' + + const output = execSync( + `${pkgManager} config get "${scope}:_authToken" ${resolvedFlags}`, + { + env: { + ...process.env, + NODE_OPTIONS: getFormattedNodeOptionsWithoutInspect(), + }, + } + ) + .toString() + .trim() + + if (output && output !== 'undefined') { + authToken = output + } + } catch { + // Auth token is not required — proceed without it + } + + return { registry, authToken } } diff --git a/packages/next/src/lib/patch-incorrect-lockfile.ts b/packages/next/src/lib/patch-incorrect-lockfile.ts index faf49a0a53a3..e175a5389e8e 100644 --- a/packages/next/src/lib/patch-incorrect-lockfile.ts +++ b/packages/next/src/lib/patch-incorrect-lockfile.ts @@ -7,11 +7,15 @@ import type { UnwrapPromise } from './coalesced-function' import { isCI } from '../server/ci-info' import { getRegistry } from './helpers/get-registry' -let registry: string | undefined +let registryConfig: ReturnType | undefined async function fetchPkgInfo(pkg: string) { - if (!registry) registry = getRegistry() - const res = await fetch(`${registry}${pkg}`) + if (!registryConfig) registryConfig = getRegistry() + const { registry, authToken } = registryConfig + const headers: HeadersInit = authToken + ? { Authorization: `Bearer ${authToken}` } + : {} + const res = await fetch(`${registry}${pkg}`, { headers }) if (!res.ok) { throw new Error( diff --git a/test/unit/get-registry.test.ts b/test/unit/get-registry.test.ts new file mode 100644 index 000000000000..b70feaf6f0a3 --- /dev/null +++ b/test/unit/get-registry.test.ts @@ -0,0 +1,81 @@ +/* eslint-env jest */ +import { execSync } from 'child_process' +import { getRegistry } from 'next/dist/lib/helpers/get-registry' + +jest.mock('child_process', () => ({ + execSync: jest.fn(), +})) + +jest.mock('next/dist/lib/helpers/get-pkg-manager', () => ({ + getPkgManager: jest.fn(() => 'npm'), +})) + +jest.mock('next/dist/server/lib/utils', () => ({ + getFormattedNodeOptionsWithoutInspect: jest.fn(() => ''), +})) + +const mockedExecSync = execSync as jest.MockedFunction + +describe('getRegistry', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return registry URL and auth token', () => { + mockedExecSync + .mockReturnValueOnce(Buffer.from('https://registry.example.com\n')) + .mockReturnValueOnce(Buffer.from('my-secret-token\n')) + + const result = getRegistry() + expect(result.registry).toBe('https://registry.example.com/') + expect(result.authToken).toBe('my-secret-token') + }) + + it('should return undefined authToken when npm returns "undefined"', () => { + mockedExecSync + .mockReturnValueOnce(Buffer.from('https://registry.npmjs.org/\n')) + .mockReturnValueOnce(Buffer.from('undefined\n')) + + const result = getRegistry() + expect(result.registry).toBe('https://registry.npmjs.org/') + expect(result.authToken).toBeUndefined() + }) + + it('should return undefined authToken when config get throws', () => { + mockedExecSync + .mockReturnValueOnce(Buffer.from('https://registry.npmjs.org/\n')) + .mockImplementationOnce(() => { + throw new Error('not found') + }) + + const result = getRegistry() + expect(result.registry).toBe('https://registry.npmjs.org/') + expect(result.authToken).toBeUndefined() + }) + + it('should query the correct npmrc key for a registry with a path', () => { + mockedExecSync + .mockReturnValueOnce( + Buffer.from('https://my.jfrog.io/artifactory/api/npm/npm/\n') + ) + .mockReturnValueOnce(Buffer.from('my-token\n')) + + const result = getRegistry() + expect(result.authToken).toBe('my-token') + expect(mockedExecSync).toHaveBeenCalledWith( + expect.stringContaining( + '//my.jfrog.io/artifactory/api/npm/npm/:_authToken' + ), + expect.any(Object) + ) + }) + + it('should fall back to default registry when config get returns non-URL', () => { + mockedExecSync + .mockReturnValueOnce(Buffer.from('WARN something\n')) + .mockReturnValueOnce(Buffer.from('undefined\n')) + + const result = getRegistry() + expect(result.registry).toBe('https://registry.npmjs.org/') + }) +})