polyfill.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // this file is a modified version of the code in node core >=14.14.0
  2. // which is, in turn, a modified version of the rimraf module on npm
  3. // node core changes:
  4. // - Use of the assert module has been replaced with core's error system.
  5. // - All code related to the glob dependency has been removed.
  6. // - Bring your own custom fs module is not currently supported.
  7. // - Some basic code cleanup.
  8. // changes here:
  9. // - remove all callback related code
  10. // - drop sync support
  11. // - change assertions back to non-internal methods (see options.js)
  12. // - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows
  13. const errnos = require('os').constants.errno
  14. const { join } = require('path')
  15. const fs = require('../fs.js')
  16. // error codes that mean we need to remove contents
  17. const notEmptyCodes = new Set([
  18. 'ENOTEMPTY',
  19. 'EEXIST',
  20. 'EPERM',
  21. ])
  22. // error codes we can retry later
  23. const retryCodes = new Set([
  24. 'EBUSY',
  25. 'EMFILE',
  26. 'ENFILE',
  27. 'ENOTEMPTY',
  28. 'EPERM',
  29. ])
  30. const isWindows = process.platform === 'win32'
  31. const defaultOptions = {
  32. retryDelay: 100,
  33. maxRetries: 0,
  34. recursive: false,
  35. force: false,
  36. }
  37. // this is drastically simplified, but should be roughly equivalent to what
  38. // node core throws
  39. class ERR_FS_EISDIR extends Error {
  40. constructor (path) {
  41. super()
  42. this.info = {
  43. code: 'EISDIR',
  44. message: 'is a directory',
  45. path,
  46. syscall: 'rm',
  47. errno: errnos.EISDIR,
  48. }
  49. this.name = 'SystemError'
  50. this.code = 'ERR_FS_EISDIR'
  51. this.errno = errnos.EISDIR
  52. this.syscall = 'rm'
  53. this.path = path
  54. this.message = `Path is a directory: ${this.syscall} returned ` +
  55. `${this.info.code} (is a directory) ${path}`
  56. }
  57. toString () {
  58. return `${this.name} [${this.code}]: ${this.message}`
  59. }
  60. }
  61. class ENOTDIR extends Error {
  62. constructor (path) {
  63. super()
  64. this.name = 'Error'
  65. this.code = 'ENOTDIR'
  66. this.errno = errnos.ENOTDIR
  67. this.syscall = 'rmdir'
  68. this.path = path
  69. this.message = `not a directory, ${this.syscall} '${this.path}'`
  70. }
  71. toString () {
  72. return `${this.name}: ${this.code}: ${this.message}`
  73. }
  74. }
  75. // force is passed separately here because we respect it for the first entry
  76. // into rimraf only, any further calls that are spawned as a result (i.e. to
  77. // delete content within the target) will ignore ENOENT errors
  78. const rimraf = async (path, options, isTop = false) => {
  79. const force = isTop ? options.force : true
  80. const stat = await fs.lstat(path)
  81. .catch((err) => {
  82. // we only ignore ENOENT if we're forcing this call
  83. if (err.code === 'ENOENT' && force) {
  84. return
  85. }
  86. if (isWindows && err.code === 'EPERM') {
  87. return fixEPERM(path, options, err, isTop)
  88. }
  89. throw err
  90. })
  91. // no stat object here means either lstat threw an ENOENT, or lstat threw
  92. // an EPERM and the fixPERM function took care of things. either way, we're
  93. // already done, so return early
  94. if (!stat) {
  95. return
  96. }
  97. if (stat.isDirectory()) {
  98. return rmdir(path, options, null, isTop)
  99. }
  100. return fs.unlink(path)
  101. .catch((err) => {
  102. if (err.code === 'ENOENT' && force) {
  103. return
  104. }
  105. if (err.code === 'EISDIR') {
  106. return rmdir(path, options, err, isTop)
  107. }
  108. if (err.code === 'EPERM') {
  109. // in windows, we handle this through fixEPERM which will also try to
  110. // delete things again. everywhere else since deleting the target as a
  111. // file didn't work we go ahead and try to delete it as a directory
  112. return isWindows
  113. ? fixEPERM(path, options, err, isTop)
  114. : rmdir(path, options, err, isTop)
  115. }
  116. throw err
  117. })
  118. }
  119. const fixEPERM = async (path, options, originalErr, isTop) => {
  120. const force = isTop ? options.force : true
  121. const targetMissing = await fs.chmod(path, 0o666)
  122. .catch((err) => {
  123. if (err.code === 'ENOENT' && force) {
  124. return true
  125. }
  126. throw originalErr
  127. })
  128. // got an ENOENT above, return now. no file = no problem
  129. if (targetMissing) {
  130. return
  131. }
  132. // this function does its own lstat rather than calling rimraf again to avoid
  133. // infinite recursion for a repeating EPERM
  134. const stat = await fs.lstat(path)
  135. .catch((err) => {
  136. if (err.code === 'ENOENT' && force) {
  137. return
  138. }
  139. throw originalErr
  140. })
  141. if (!stat) {
  142. return
  143. }
  144. if (stat.isDirectory()) {
  145. return rmdir(path, options, originalErr, isTop)
  146. }
  147. return fs.unlink(path)
  148. }
  149. const rmdir = async (path, options, originalErr, isTop) => {
  150. if (!options.recursive && isTop) {
  151. throw originalErr || new ERR_FS_EISDIR(path)
  152. }
  153. const force = isTop ? options.force : true
  154. return fs.rmdir(path)
  155. .catch(async (err) => {
  156. // in Windows, calling rmdir on a file path will fail with ENOENT rather
  157. // than ENOTDIR. to determine if that's what happened, we have to do
  158. // another lstat on the path. if the path isn't actually gone, we throw
  159. // away the ENOENT and replace it with our own ENOTDIR
  160. if (isWindows && err.code === 'ENOENT') {
  161. const stillExists = await fs.lstat(path).then(() => true, () => false)
  162. if (stillExists) {
  163. err = new ENOTDIR(path)
  164. }
  165. }
  166. // not there, not a problem
  167. if (err.code === 'ENOENT' && force) {
  168. return
  169. }
  170. // we may not have originalErr if lstat tells us our target is a
  171. // directory but that changes before we actually remove it, so
  172. // only throw it here if it's set
  173. if (originalErr && err.code === 'ENOTDIR') {
  174. throw originalErr
  175. }
  176. // the directory isn't empty, remove the contents and try again
  177. if (notEmptyCodes.has(err.code)) {
  178. const files = await fs.readdir(path)
  179. await Promise.all(files.map((file) => {
  180. const target = join(path, file)
  181. return rimraf(target, options)
  182. }))
  183. return fs.rmdir(path)
  184. }
  185. throw err
  186. })
  187. }
  188. const rm = async (path, opts) => {
  189. const options = { ...defaultOptions, ...opts }
  190. let retries = 0
  191. const errHandler = async (err) => {
  192. if (retryCodes.has(err.code) && ++retries < options.maxRetries) {
  193. const delay = retries * options.retryDelay
  194. await promiseTimeout(delay)
  195. return rimraf(path, options, true).catch(errHandler)
  196. }
  197. throw err
  198. }
  199. return rimraf(path, options, true).catch(errHandler)
  200. }
  201. const promiseTimeout = (ms) => new Promise((r) => setTimeout(r, ms))
  202. module.exports = rm