Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .chronus/changes/fix-tsp-install-npmrc-2026-7-14-0-45-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

`tsp install` now respects the `.npmrc` configuration(project, user and global config as well as `npm_config_*` environment variables) when resolving and downloading the package manager. Custom registries(including scoped registries) and their credentials(`_authToken`, `_auth` and `username`/`_password`) are now used.
25 changes: 23 additions & 2 deletions packages/compiler/src/install/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getDirectoryPath, joinPaths } from "../core/path-utils.js";
import { NoTarget, type Diagnostic, type Tracer } from "../core/types.js";
import { downloadAndExtractPackage } from "../package-manger/npm-package-download.js";
import { fetchPackageManifest, type NpmManifest } from "../package-manger/npm-registry.js";
import { resolveNpmConfig, type NpmConfig } from "../package-manger/npmrc.js";
import { mkTempDir } from "../utils/fs-utils.js";
import type { SupportedPackageManager } from "./config.js";
import { getPackageManagerConfig, type PackageManagerConfig } from "./config.js";
Expand Down Expand Up @@ -91,6 +92,7 @@ async function installPackageManager(
spec: Descriptor,
installDir: string,
manifest: NpmManifest,
npmConfig: NpmConfig,
) {
await rm(installDir, { recursive: true, force: true });
const tempDir = await mkTempDir(host, pmDir, `tsp-pm-${packageManager}-${manifest.version}`);
Expand All @@ -99,7 +101,12 @@ async function installPackageManager(
"downloading-extracting",
`Downloading and extracting ${packageManager} at version ${manifest.version} in ${tempDir}`,
);
const extractResult = await downloadAndExtractPackage(manifest, tempDir, spec.hash?.algorithm);
const extractResult = await downloadAndExtractPackage(
manifest,
tempDir,
spec.hash?.algorithm,
npmConfig.getAuthHeaders(manifest.dist.tarball),
);
if (spec.hash) {
if (spec.hash.value !== extractResult.hash.value) {
throw new InstallDependenciesError(
Expand Down Expand Up @@ -165,7 +172,20 @@ export async function installTypeSpecDependencies(
);
const packageManager = spec.name;
const packageManagerConfig = getPackageManagerConfig(packageManager);
const manifest = await fetchPackageManifest(packageManager, spec.range);
const npmConfig = await resolveNpmConfig(host, directory);
const registry = npmConfig.getRegistry(packageManager);
tracer.trace("registry", `Using registry ${registry} to resolve ${packageManager}`);
let manifest: NpmManifest;
try {
manifest = await fetchPackageManifest(packageManager, spec.range, {
registry,
headers: npmConfig.getAuthHeaders(`${registry}/${packageManager}`),
});
} catch (e) {
throw new InstallDependenciesError(
`Failed to resolve package manager ${packageManager}@${spec.range} from registry ${registry}: ${e instanceof Error ? e.message : String(e)}`,
);
}
tracer.trace(
"fetched-manifest",
`Resolved manifest for ${packageManager} at version ${manifest.version}`,
Expand All @@ -182,6 +202,7 @@ export async function installTypeSpecDependencies(
spec,
installDir,
manifest,
npmConfig,
);
if (savePackageManager) {
await updatePackageManagerInPackageJson(host, packageJsonPath, {
Expand Down
32 changes: 27 additions & 5 deletions packages/compiler/src/package-manger/npm-package-download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,39 @@ import { createHash } from "crypto";
import { Readable } from "stream";
import { extract as tarX } from "tar/extract";
import type { Hash } from "../install/spec.js";
import { fetchPackageManifest, type NpmManifest } from "./npm-registry.js";
import {
fetchPackageManifest,
type NpmManifest,
type NpmRegistryRequestOptions,
} from "./npm-registry.js";

export async function downloadPackageVersion(
packageName: string,
version: string,
dest: string,
options?: NpmRegistryRequestOptions,
): Promise<ExtractedTarballResult> {
const manifest = await fetchPackageManifest(packageName, version);
return downloadAndExtractTarball(manifest.dist.tarball, dest);
const manifest = await fetchPackageManifest(packageName, version, options);
// Only forward the credentials to the tarball if it is served by the same registry.
const headers = isSameOrigin(manifest.dist.tarball, options?.registry)
? options?.headers
: undefined;
return downloadAndExtractTarball(manifest.dist.tarball, dest, undefined, headers);
}

export async function downloadAndExtractPackage(
manifest: NpmManifest,
dest: string,
hashAlgorithm: string = "sha512",
headers?: Record<string, string>,
): Promise<ExtractedTarballResult> {
return downloadAndExtractTarball(manifest.dist.tarball, dest, hashAlgorithm);
return downloadAndExtractTarball(manifest.dist.tarball, dest, hashAlgorithm, headers);
}

function isSameOrigin(url: string, other: string | undefined): boolean {
if (other === undefined) return false;
if (!URL.canParse(url) || !URL.canParse(other)) return false;
return new URL(url).origin === new URL(other).origin;
}

export interface ExtractedTarballResult {
Expand All @@ -31,8 +47,14 @@ async function downloadAndExtractTarball(
url: string,
dest: string,
hashAlgorithm: string = "sha512",
headers?: Record<string, string>,
): Promise<ExtractedTarballResult> {
const res = await fetch(url);
const res = await fetch(url, { headers });
if (!res.ok || res.body === null) {
throw new Error(
`Failed to download package tarball from ${url}: ${res.status} ${res.statusText}`,
);
}
const tarballStream = Readable.fromWeb(res.body as any);
const hash = tarballStream.pipe(createHash(hashAlgorithm));
const extractor = tarX({
Expand Down
31 changes: 25 additions & 6 deletions packages/compiler/src/package-manger/npm-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,45 @@ export interface NpmHuman {
readonly url?: string | undefined;
}

const defaultRegistry = `https://registry.npmjs.org`;
/** Default npm registry used when nothing else is configured. */
export const defaultNpmRegistry = `https://registry.npmjs.org`;

/**
* Returns the npm registry URL to use for fetching packages.
* Uses the `TYPESPEC_NPM_REGISTRY` environment variable if set,
* otherwise falls back to the default npm registry.
*/
export function getNpmRegistry(): string {
return (process.env["TYPESPEC_NPM_REGISTRY"] ?? defaultRegistry).replace(/\/$/, "");
return (process.env["TYPESPEC_NPM_REGISTRY"] ?? defaultNpmRegistry).replace(/\/$/, "");
}

/** Options to customize the registry and credentials used when talking to the npm registry. */
export interface NpmRegistryRequestOptions {
/** Registry to use. Default to {@link getNpmRegistry} */
readonly registry?: string;
/** Extra headers to send with the request.(e.g. authorization header for private registries) */
readonly headers?: Record<string, string>;
}

export async function fetchPackageManifest(
packageName: string,
version: string,
options?: NpmRegistryRequestOptions,
): Promise<NpmManifest> {
const url = `${getNpmRegistry()}/${packageName}/${version}`;
const res = await fetch(url);
const registry = options?.registry ? options.registry.replace(/\/+$/, "") : getNpmRegistry();
const url = `${registry}/${packageName}/${version}`;
const res = await fetch(url, { headers: options?.headers });
if (!res.ok) {
throw new Error(
`Failed to fetch manifest for package "${packageName}@${version}" from ${registry}: ${res.status} ${res.statusText}`,
);
}
return await res.json();
}

export function fetchLatestPackageManifest(packageName: string): Promise<NpmManifest> {
return fetchPackageManifest(packageName, "latest");
export function fetchLatestPackageManifest(
packageName: string,
options?: NpmRegistryRequestOptions,
): Promise<NpmManifest> {
return fetchPackageManifest(packageName, "latest", options);
}
Loading
Loading