io.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. let { rm, existsSync } = require('fs')
  2. let path = require('path')
  3. let fs = require('fs/promises')
  4. let chokidar = require('chokidar')
  5. let resolveToolRoot = require('./resolve-tool-root')
  6. let FILE_STATE = {
  7. NotFound: Symbol(),
  8. }
  9. function getWatcherOptions() {
  10. return {
  11. usePolling: true,
  12. interval: 200,
  13. awaitWriteFinish: {
  14. stabilityThreshold: 1500,
  15. pollInterval: 50,
  16. },
  17. }
  18. }
  19. module.exports = function ({
  20. /** Output directory, relative to the tool. */
  21. output = 'dist',
  22. /** Input directory, relative to the tool. */
  23. input = 'src',
  24. /** Whether or not you want to cleanup the output directory. */
  25. cleanup = true,
  26. } = {}) {
  27. let toolRoot = resolveToolRoot()
  28. let fileCache = new Map()
  29. let absoluteOutputFolder = path.resolve(toolRoot, output)
  30. let absoluteInputFolder = path.resolve(toolRoot, input)
  31. if (cleanup) {
  32. beforeAll((done) => rm(absoluteOutputFolder, { recursive: true, force: true }, done))
  33. afterEach((done) => rm(absoluteOutputFolder, { recursive: true, force: true }, done))
  34. }
  35. // Restore all written files
  36. afterEach(async () => {
  37. await Promise.all(
  38. Array.from(fileCache.entries()).map(async ([file, content]) => {
  39. try {
  40. if (content === FILE_STATE.NotFound) {
  41. return await fs.unlink(file)
  42. } else {
  43. return await fs.writeFile(file, content, 'utf8')
  44. }
  45. } catch {}
  46. })
  47. )
  48. })
  49. async function readdir(start, parent = []) {
  50. let files = await fs.readdir(start, { withFileTypes: true })
  51. let resolvedFiles = await Promise.all(
  52. files.map((file) => {
  53. if (file.isDirectory()) {
  54. return readdir(path.resolve(start, file.name), [...parent, file.name])
  55. }
  56. return parent.concat(file.name).join(path.sep)
  57. })
  58. )
  59. return resolvedFiles.flat(Infinity)
  60. }
  61. async function resolveFile(fileOrRegex, directory) {
  62. if (fileOrRegex instanceof RegExp) {
  63. let files = await readdir(directory)
  64. if (files.length === 0) {
  65. throw new Error(`No files exists in "${directory}"`)
  66. }
  67. let filtered = files.filter((file) => fileOrRegex.test(file))
  68. if (filtered.length === 0) {
  69. throw new Error(`Not a single file matched: ${fileOrRegex}`)
  70. } else if (filtered.length > 1) {
  71. throw new Error(`Multiple files matched: ${fileOrRegex}`)
  72. }
  73. return filtered[0]
  74. }
  75. return fileOrRegex
  76. }
  77. return {
  78. cleanupFile(file) {
  79. let filePath = path.resolve(toolRoot, file)
  80. fileCache.set(filePath, FILE_STATE.NotFound)
  81. },
  82. async fileExists(file) {
  83. let filePath = path.resolve(toolRoot, file)
  84. return existsSync(filePath)
  85. },
  86. async removeFile(file) {
  87. let filePath = path.resolve(toolRoot, file)
  88. if (!fileCache.has(filePath)) {
  89. fileCache.set(
  90. filePath,
  91. await fs.readFile(filePath, 'utf8').catch(() => FILE_STATE.NotFound)
  92. )
  93. }
  94. await fs.unlink(filePath).catch(() => null)
  95. },
  96. async readOutputFile(file) {
  97. file = await resolveFile(file, absoluteOutputFolder)
  98. return fs.readFile(path.resolve(absoluteOutputFolder, file), 'utf8')
  99. },
  100. async readInputFile(file) {
  101. file = await resolveFile(file, absoluteInputFolder)
  102. return fs.readFile(path.resolve(absoluteInputFolder, file), 'utf8')
  103. },
  104. async appendToInputFile(file, contents) {
  105. let filePath = path.resolve(absoluteInputFolder, file)
  106. if (!fileCache.has(filePath)) {
  107. fileCache.set(filePath, await fs.readFile(filePath, 'utf8'))
  108. }
  109. return fs.appendFile(filePath, contents, 'utf8')
  110. },
  111. async writeInputFile(file, contents) {
  112. let filePath = path.resolve(absoluteInputFolder, file)
  113. // Ensure the parent folder of the file exists
  114. if (
  115. !(await fs
  116. .stat(filePath)
  117. .then(() => true)
  118. .catch(() => false))
  119. ) {
  120. await fs.mkdir(path.dirname(filePath), { recursive: true })
  121. }
  122. if (!fileCache.has(filePath)) {
  123. try {
  124. fileCache.set(filePath, await fs.readFile(filePath, 'utf8'))
  125. } catch (err) {
  126. if (err.code === 'ENOENT') {
  127. fileCache.set(filePath, FILE_STATE.NotFound)
  128. } else {
  129. throw err
  130. }
  131. }
  132. }
  133. return fs.writeFile(path.resolve(absoluteInputFolder, file), contents, 'utf8')
  134. },
  135. async waitForOutputFileCreation(file) {
  136. if (file instanceof RegExp) {
  137. let r = file
  138. let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
  139. return new Promise((resolve) => {
  140. watcher.on('add', (file) => {
  141. if (r.test(file)) {
  142. watcher.close().then(() => resolve())
  143. }
  144. })
  145. })
  146. } else {
  147. let filePath = path.resolve(absoluteOutputFolder, file)
  148. return new Promise((resolve) => {
  149. let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
  150. watcher.on('add', (addedFile) => {
  151. if (addedFile !== filePath) return
  152. return watcher.close().finally(resolve)
  153. })
  154. })
  155. }
  156. },
  157. async waitForOutputFileChange(file, cb = () => {}) {
  158. file = await resolveFile(file, absoluteOutputFolder)
  159. let filePath = path.resolve(absoluteOutputFolder, file)
  160. return new Promise((resolve) => {
  161. let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
  162. watcher
  163. .on('change', (changedFile) => {
  164. if (changedFile !== filePath) return
  165. return watcher.close().finally(resolve)
  166. })
  167. .on('ready', cb)
  168. })
  169. },
  170. }
  171. }