package-json-reader.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. // Manually “tree shaken” from:
  2. // <https://github.com/nodejs/node/blob/7c3dce0/lib/internal/modules/package_json_reader.js>
  3. // Last checked on: Apr 29, 2024.
  4. // Removed the native dependency.
  5. // Also: no need to cache, we do that in resolve already.
  6. /**
  7. * @import {ErrnoException} from './errors.js'
  8. *
  9. * @typedef {'commonjs' | 'module' | 'none'} PackageType
  10. *
  11. * @typedef PackageConfig
  12. * @property {string} pjsonPath
  13. * @property {boolean} exists
  14. * @property {string | undefined} [main]
  15. * @property {string | undefined} [name]
  16. * @property {PackageType} type
  17. * @property {Record<string, unknown> | undefined} [exports]
  18. * @property {Record<string, unknown> | undefined} [imports]
  19. */
  20. import fs from 'node:fs'
  21. import path from 'node:path'
  22. import {fileURLToPath} from 'node:url'
  23. import {codes} from './errors.js'
  24. const hasOwnProperty = {}.hasOwnProperty
  25. const {ERR_INVALID_PACKAGE_CONFIG} = codes
  26. /** @type {Map<string, PackageConfig>} */
  27. const cache = new Map()
  28. /**
  29. * @param {string} jsonPath
  30. * @param {{specifier: URL | string, base?: URL}} options
  31. * @returns {PackageConfig}
  32. */
  33. export function read(jsonPath, {base, specifier}) {
  34. const existing = cache.get(jsonPath)
  35. if (existing) {
  36. return existing
  37. }
  38. /** @type {string | undefined} */
  39. let string
  40. try {
  41. string = fs.readFileSync(path.toNamespacedPath(jsonPath), 'utf8')
  42. } catch (error) {
  43. const exception = /** @type {ErrnoException} */ (error)
  44. if (exception.code !== 'ENOENT') {
  45. throw exception
  46. }
  47. }
  48. /** @type {PackageConfig} */
  49. const result = {
  50. exists: false,
  51. pjsonPath: jsonPath,
  52. main: undefined,
  53. name: undefined,
  54. type: 'none', // Ignore unknown types for forwards compatibility
  55. exports: undefined,
  56. imports: undefined
  57. }
  58. if (string !== undefined) {
  59. /** @type {Record<string, unknown>} */
  60. let parsed
  61. try {
  62. parsed = JSON.parse(string)
  63. } catch (error_) {
  64. const cause = /** @type {ErrnoException} */ (error_)
  65. const error = new ERR_INVALID_PACKAGE_CONFIG(
  66. jsonPath,
  67. (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier),
  68. cause.message
  69. )
  70. error.cause = cause
  71. throw error
  72. }
  73. result.exists = true
  74. if (
  75. hasOwnProperty.call(parsed, 'name') &&
  76. typeof parsed.name === 'string'
  77. ) {
  78. result.name = parsed.name
  79. }
  80. if (
  81. hasOwnProperty.call(parsed, 'main') &&
  82. typeof parsed.main === 'string'
  83. ) {
  84. result.main = parsed.main
  85. }
  86. if (hasOwnProperty.call(parsed, 'exports')) {
  87. // @ts-expect-error: assume valid.
  88. result.exports = parsed.exports
  89. }
  90. if (hasOwnProperty.call(parsed, 'imports')) {
  91. // @ts-expect-error: assume valid.
  92. result.imports = parsed.imports
  93. }
  94. // Ignore unknown types for forwards compatibility
  95. if (
  96. hasOwnProperty.call(parsed, 'type') &&
  97. (parsed.type === 'commonjs' || parsed.type === 'module')
  98. ) {
  99. result.type = parsed.type
  100. }
  101. }
  102. cache.set(jsonPath, result)
  103. return result
  104. }
  105. /**
  106. * @param {URL | string} resolved
  107. * @returns {PackageConfig}
  108. */
  109. export function getPackageScopeConfig(resolved) {
  110. // Note: in Node, this is now a native module.
  111. let packageJSONUrl = new URL('package.json', resolved)
  112. while (true) {
  113. const packageJSONPath = packageJSONUrl.pathname
  114. if (packageJSONPath.endsWith('node_modules/package.json')) {
  115. break
  116. }
  117. const packageConfig = read(fileURLToPath(packageJSONUrl), {
  118. specifier: resolved
  119. })
  120. if (packageConfig.exists) {
  121. return packageConfig
  122. }
  123. const lastPackageJSONUrl = packageJSONUrl
  124. packageJSONUrl = new URL('../package.json', packageJSONUrl)
  125. // Terminates at root where ../package.json equals ../../package.json
  126. // (can't just check "/package.json" for Windows support).
  127. if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) {
  128. break
  129. }
  130. }
  131. const packageJSONPath = fileURLToPath(packageJSONUrl)
  132. // ^^ Note: in Node, this is now a native module.
  133. return {
  134. pjsonPath: packageJSONPath,
  135. exists: false,
  136. type: 'none'
  137. }
  138. }
  139. /**
  140. * Returns the package type for a given URL.
  141. * @param {URL} url - The URL to get the package type for.
  142. * @returns {PackageType}
  143. */
  144. export function getPackageType(url) {
  145. // To do @anonrig: Write a C++ function that returns only "type".
  146. return getPackageScopeConfig(url).type
  147. }