diff --git a/.chronus/changes/forward-typespec-npm-registry-2026-08-15.md b/.chronus/changes/forward-typespec-npm-registry-2026-08-15.md new file mode 100644 index 00000000000..2152f7a454b --- /dev/null +++ b/.chronus/changes/forward-typespec-npm-registry-2026-08-15.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Forward `TYPESPEC_NPM_REGISTRY` to the npm process launched by `tsp install`. diff --git a/packages/compiler/src/install/install.ts b/packages/compiler/src/install/install.ts index 9460879ee66..ea2ee69e069 100644 --- a/packages/compiler/src/install/install.ts +++ b/packages/compiler/src/install/install.ts @@ -7,7 +7,11 @@ import { createDiagnosticCollector } from "../core/diagnostics.js"; 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 { + fetchPackageManifest, + getNpmRegistryEnvironment, + type NpmManifest, +} from "../package-manger/npm-registry.js"; import { mkTempDir } from "../utils/fs-utils.js"; import type { SupportedPackageManager } from "./config.js"; import { getPackageManagerConfig, type PackageManagerConfig } from "./config.js"; @@ -218,7 +222,7 @@ async function runPackageManager( stdio, cwd: directory, env: { - ...process.env, + ...getNpmRegistryEnvironment(), TYPESPEC_CLI_PASSTHROUGH: "1", }, }); diff --git a/packages/compiler/src/package-manger/npm-registry.ts b/packages/compiler/src/package-manger/npm-registry.ts index 4672eafe669..b12444aad62 100644 --- a/packages/compiler/src/package-manger/npm-registry.ts +++ b/packages/compiler/src/package-manger/npm-registry.ts @@ -90,7 +90,26 @@ const defaultRegistry = `https://registry.npmjs.org`; * otherwise falls back to the default npm registry. */ export function getNpmRegistry(): string { - return (process.env["TYPESPEC_NPM_REGISTRY"] ?? defaultRegistry).replace(/\/$/, ""); + const registry = process.env["TYPESPEC_NPM_REGISTRY"]?.trim(); + return (registry || defaultRegistry).replace(/\/$/, ""); +} + +/** Returns the environment used to run npm with the TypeSpec registry override applied. */ +export function getNpmRegistryEnvironment(): NodeJS.ProcessEnv { + const environment = { ...process.env }; + if (!process.env["TYPESPEC_NPM_REGISTRY"]?.trim()) { + return environment; + } + + // Environment variable names are case-insensitive on Windows. Remove any + // existing spelling so Node does not choose between duplicate keys. + for (const name of Object.keys(environment)) { + if (name.toLowerCase() === "npm_config_registry") { + delete environment[name]; + } + } + environment["npm_config_registry"] = getNpmRegistry(); + return environment; } export async function fetchPackageManifest( diff --git a/packages/compiler/test/package-manager/npm-registry.test.ts b/packages/compiler/test/package-manager/npm-registry.test.ts index 02092ef5b98..f9c0edfaef9 100644 --- a/packages/compiler/test/package-manager/npm-registry.test.ts +++ b/packages/compiler/test/package-manager/npm-registry.test.ts @@ -1,7 +1,11 @@ import * as http from "http"; import type { AddressInfo } from "net"; import { afterEach, beforeEach, expect, it } from "vitest"; -import { fetchPackageManifest } from "../../src/package-manger/npm-registry.js"; +import { + fetchPackageManifest, + getNpmRegistry, + getNpmRegistryEnvironment, +} from "../../src/package-manger/npm-registry.js"; let server: http.Server; let registryUrl: string; @@ -34,6 +38,7 @@ beforeEach(async () => { afterEach(async () => { delete process.env["TYPESPEC_NPM_REGISTRY"]; + delete process.env["NPM_CONFIG_REGISTRY"]; await new Promise((resolve) => server.close(() => resolve())); }); @@ -50,3 +55,33 @@ it("strips trailing slash from TYPESPEC_NPM_REGISTRY", async () => { expect(manifest.name).toBe("test-pkg"); expect(lastRequestUrl).toBe("/test-pkg/1.0.0"); }); + +it("forwards TYPESPEC_NPM_REGISTRY to npm", () => { + process.env["TYPESPEC_NPM_REGISTRY"] = `${registryUrl}/`; + process.env["NPM_CONFIG_REGISTRY"] = "https://old-registry.example.com"; + + const environment = getNpmRegistryEnvironment(); + + expect(environment["npm_config_registry"]).toBe(registryUrl); + expect(environment).not.toHaveProperty("NPM_CONFIG_REGISTRY"); +}); + +it("preserves the npm registry environment when no TypeSpec override is set", () => { + delete process.env["TYPESPEC_NPM_REGISTRY"]; + process.env["NPM_CONFIG_REGISTRY"] = "https://configured-registry.example.com"; + + const environment = getNpmRegistryEnvironment(); + + expect(environment["NPM_CONFIG_REGISTRY"]).toBe("https://configured-registry.example.com"); +}); + +it.each(["", " "])("does not forward an empty TypeSpec registry override", (registry) => { + process.env["TYPESPEC_NPM_REGISTRY"] = registry; + process.env["NPM_CONFIG_REGISTRY"] = "https://configured-registry.example.com"; + + const environment = getNpmRegistryEnvironment(); + + expect(getNpmRegistry()).toBe("https://registry.npmjs.org"); + expect(environment["NPM_CONFIG_REGISTRY"]).toBe("https://configured-registry.example.com"); + expect(environment).not.toHaveProperty("npm_config_registry"); +});