open-url.js 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. const { open } = require('@npmcli/promise-spawn')
  2. const { output, input, META } = require('proc-log')
  3. const { URL } = require('node:url')
  4. const readline = require('node:readline/promises')
  5. const { once } = require('node:events')
  6. const assertValidUrl = (url) => {
  7. try {
  8. if (!/^https?:$/.test(new URL(url).protocol)) {
  9. throw new Error()
  10. }
  11. } catch {
  12. throw new Error('Invalid URL: ' + url)
  13. }
  14. }
  15. const outputMsg = (json, title, url) => {
  16. if (json) {
  17. output.buffer({ title, url })
  18. } else {
  19. // These urls are sometimes specifically login urls so we have to turn off redaction to standard output
  20. output.standard(`${title}:\n${url}`, { [META]: true, redact: false })
  21. }
  22. }
  23. // attempt to open URL in web-browser, print address otherwise:
  24. const openUrl = async (npm, url, title, isFile) => {
  25. url = encodeURI(url)
  26. const browser = npm.config.get('browser')
  27. const json = npm.config.get('json')
  28. if (browser === false) {
  29. outputMsg(json, title, url)
  30. return
  31. }
  32. // We pass this in as true from the help command so we know we don't have to check the protocol
  33. if (!isFile) {
  34. assertValidUrl(url)
  35. }
  36. try {
  37. await input.start(() => open(url, {
  38. command: browser === true ? null : browser,
  39. }))
  40. } catch (err) {
  41. if (err.code !== 127) {
  42. throw err
  43. }
  44. outputMsg(json, title, url)
  45. }
  46. }
  47. // Prompt to open URL in browser if possible
  48. const openUrlPrompt = async (npm, url, title, prompt, { signal }) => {
  49. const browser = npm.config.get('browser')
  50. const json = npm.config.get('json')
  51. assertValidUrl(url)
  52. outputMsg(json, title, url)
  53. if (browser === false || !process.stdin.isTTY || !process.stdout.isTTY) {
  54. return
  55. }
  56. const rl = readline.createInterface({
  57. input: process.stdin,
  58. output: process.stdout,
  59. })
  60. try {
  61. await input.read(() => Promise.race([
  62. rl.question(prompt, { signal }),
  63. once(rl, 'error'),
  64. once(rl, 'SIGINT').then(() => {
  65. throw new Error('canceled')
  66. }),
  67. ]))
  68. rl.close()
  69. await openUrl(npm, url, 'Browser unavailable. Please open the URL manually')
  70. } catch (err) {
  71. rl.close()
  72. if (err.name !== 'AbortError') {
  73. throw err
  74. }
  75. }
  76. }
  77. // Rearrange arguments and return a function that takes the two arguments returned from the npm-profile methods that take an opener
  78. const createOpener = (npm, title, prompt = 'Press ENTER to open in the browser...') =>
  79. (url, opts) => openUrlPrompt(npm, url, title, prompt, opts)
  80. module.exports = {
  81. openUrl,
  82. openUrlPrompt,
  83. createOpener,
  84. }