mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-14 03:26:13 -04:00
* feat: add pnpm docs command and home alias * chore: update manifests * fix: address review comments for pnpm docs command - Remove 'docs' and 'home' from NOT_IMPLEMENTED_COMMANDS to prevent not-implemented handlers from overriding the real implementations - Change fallback URL from npmjs.com to npmx.dev - Remove bugs URL as a docs fallback (it's an issue tracker, not docs) - Fix ESM mock in tests: use jest.unstable_mockModule instead of jest.mock * refactor: use open package for browser opening in promptBrowserOpen Replace the hand-rolled platform-specific execFile logic with the open npm package, which handles WSL, Docker-in-WSL, and Windows edge cases better. This removes the execFile dependency injection from promptBrowserOpen, OtpContext, LoginContext, and SharedContext. * fix: address copilot review comments - docs: use www.npmjs.com fallback (not npmx.dev) and validate homepage URL is http(s) before opening - promptBrowserOpen: validate authUrl protocol before passing to open(), and guard against synchronous throws from open() * fix: restore npmx.dev fallback for pnpm docs * chore: expand changeset with web-auth refactor impact --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
import open from 'open'
|
|
|
|
export interface PromptBrowserOpenReadlineInterface {
|
|
once: (event: string, listener: () => void) => void
|
|
close: () => void
|
|
}
|
|
|
|
export interface PromptBrowserOpenContext {
|
|
createReadlineInterface?: () => PromptBrowserOpenReadlineInterface
|
|
globalInfo: (message: string) => void
|
|
globalWarn: (message: string) => void
|
|
process: { stdin: { isTTY?: boolean } }
|
|
}
|
|
|
|
export interface PromptBrowserOpenParams {
|
|
authUrl: string
|
|
context: PromptBrowserOpenContext
|
|
pollPromise: Promise<string>
|
|
}
|
|
|
|
/**
|
|
* Wraps a token-polling promise with an optional "Press ENTER to open in
|
|
* browser" prompt.
|
|
*
|
|
* While the poll runs in the background, listens for the user pressing Enter
|
|
* to open the authentication URL in their browser. When the poll completes
|
|
* (regardless of whether the user pressed Enter), the keyboard listener is
|
|
* cleaned up.
|
|
*
|
|
* Error-tolerant: failures in the keyboard listener or browser opening are
|
|
* logged as warnings and do not interrupt the poll.
|
|
*/
|
|
export async function promptBrowserOpen ({
|
|
authUrl,
|
|
context,
|
|
pollPromise,
|
|
}: PromptBrowserOpenParams): Promise<string> {
|
|
const { createReadlineInterface, globalInfo, globalWarn, process } = context
|
|
|
|
if (!createReadlineInterface || !process.stdin.isTTY) {
|
|
return pollPromise
|
|
}
|
|
|
|
// The authUrl comes from an untrusted registry response, so only allow
|
|
// http(s) URLs through to `open()`.
|
|
let canonicalUrl: string
|
|
try {
|
|
const parsed = new URL(authUrl)
|
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
return pollPromise
|
|
}
|
|
canonicalUrl = parsed.href
|
|
} catch {
|
|
return pollPromise
|
|
}
|
|
|
|
let rl: PromptBrowserOpenReadlineInterface
|
|
try {
|
|
rl = createReadlineInterface()
|
|
} catch (err) {
|
|
globalWarn(`Could not set up keyboard listener: ${String(err)}`)
|
|
return pollPromise
|
|
}
|
|
|
|
globalInfo('Press ENTER to open the URL in your browser.')
|
|
|
|
rl.once('line', () => {
|
|
const handleOpenError = (err: unknown): void => {
|
|
globalWarn(`Could not open browser automatically: ${String(err)}`)
|
|
globalInfo('Please open the URL shown above manually.')
|
|
}
|
|
try {
|
|
open(canonicalUrl).catch(handleOpenError)
|
|
} catch (err) {
|
|
handleOpenError(err)
|
|
}
|
|
})
|
|
|
|
// Only await pollPromise — do NOT await the Enter keypress.
|
|
//
|
|
// The Enter listener is a fire-and-forget side effect. Users may authenticate
|
|
// on their phone (via QR code or pasted URL) without ever pressing Enter, so
|
|
// the poll must be able to complete independently.
|
|
//
|
|
// npm uses Promise.all([opener, poll]) which blocks the entire flow until the
|
|
// user presses Enter — even if authentication already succeeded on another
|
|
// device: <https://github.com/npm/npm-profile/blob/d1a48be4/lib/index.js#L85-L98>
|
|
try {
|
|
return await pollPromise
|
|
} finally {
|
|
rl.close()
|
|
}
|
|
}
|