@plugjs/plug 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/LICENSE.md +211 -0
  2. package/NOTICE.md +13 -0
  3. package/README.md +7 -0
  4. package/dist/assert.cjs +72 -0
  5. package/dist/assert.cjs.map +6 -0
  6. package/dist/assert.mjs +41 -0
  7. package/dist/assert.mjs.map +6 -0
  8. package/dist/async.cjs +58 -0
  9. package/dist/async.cjs.map +6 -0
  10. package/dist/async.mjs +30 -0
  11. package/dist/async.mjs.map +6 -0
  12. package/dist/build.cjs +136 -0
  13. package/dist/build.cjs.map +6 -0
  14. package/dist/build.mjs +110 -0
  15. package/dist/build.mjs.map +6 -0
  16. package/dist/files.cjs +113 -0
  17. package/dist/files.cjs.map +6 -0
  18. package/dist/files.mjs +88 -0
  19. package/dist/files.mjs.map +6 -0
  20. package/dist/fork.cjs +177 -0
  21. package/dist/fork.cjs.map +6 -0
  22. package/dist/fork.mjs +151 -0
  23. package/dist/fork.mjs.map +6 -0
  24. package/dist/helpers.cjs +129 -0
  25. package/dist/helpers.cjs.map +6 -0
  26. package/dist/helpers.mjs +97 -0
  27. package/dist/helpers.mjs.map +6 -0
  28. package/dist/index.cjs +25 -0
  29. package/dist/index.cjs.map +6 -0
  30. package/dist/index.mjs +7 -0
  31. package/dist/index.mjs.map +6 -0
  32. package/dist/log/colors.cjs +129 -0
  33. package/dist/log/colors.cjs.map +6 -0
  34. package/dist/log/colors.mjs +93 -0
  35. package/dist/log/colors.mjs.map +6 -0
  36. package/dist/log/emit.cjs +109 -0
  37. package/dist/log/emit.cjs.map +6 -0
  38. package/dist/log/emit.mjs +83 -0
  39. package/dist/log/emit.mjs.map +6 -0
  40. package/dist/log/levels.cjs +75 -0
  41. package/dist/log/levels.cjs.map +6 -0
  42. package/dist/log/levels.mjs +41 -0
  43. package/dist/log/levels.mjs.map +6 -0
  44. package/dist/log/logger.cjs +129 -0
  45. package/dist/log/logger.cjs.map +6 -0
  46. package/dist/log/logger.mjs +104 -0
  47. package/dist/log/logger.mjs.map +6 -0
  48. package/dist/log/options.cjs +149 -0
  49. package/dist/log/options.cjs.map +6 -0
  50. package/dist/log/options.mjs +124 -0
  51. package/dist/log/options.mjs.map +6 -0
  52. package/dist/log/report.cjs +284 -0
  53. package/dist/log/report.cjs.map +6 -0
  54. package/dist/log/report.mjs +259 -0
  55. package/dist/log/report.mjs.map +6 -0
  56. package/dist/log/spinner.cjs +83 -0
  57. package/dist/log/spinner.cjs.map +6 -0
  58. package/dist/log/spinner.mjs +57 -0
  59. package/dist/log/spinner.mjs.map +6 -0
  60. package/dist/log.cjs +71 -0
  61. package/dist/log.cjs.map +6 -0
  62. package/dist/log.mjs +45 -0
  63. package/dist/log.mjs.map +6 -0
  64. package/dist/paths.cjs +158 -0
  65. package/dist/paths.cjs.map +6 -0
  66. package/dist/paths.mjs +122 -0
  67. package/dist/paths.mjs.map +6 -0
  68. package/dist/pipe.cjs +71 -0
  69. package/dist/pipe.cjs.map +6 -0
  70. package/dist/pipe.mjs +44 -0
  71. package/dist/pipe.mjs.map +6 -0
  72. package/dist/plugs/copy.cjs +95 -0
  73. package/dist/plugs/copy.cjs.map +6 -0
  74. package/dist/plugs/copy.mjs +64 -0
  75. package/dist/plugs/copy.mjs.map +6 -0
  76. package/dist/plugs/coverage/analysis.cjs +229 -0
  77. package/dist/plugs/coverage/analysis.cjs.map +6 -0
  78. package/dist/plugs/coverage/analysis.mjs +202 -0
  79. package/dist/plugs/coverage/analysis.mjs.map +6 -0
  80. package/dist/plugs/coverage/report.cjs +215 -0
  81. package/dist/plugs/coverage/report.cjs.map +6 -0
  82. package/dist/plugs/coverage/report.mjs +200 -0
  83. package/dist/plugs/coverage/report.mjs.map +6 -0
  84. package/dist/plugs/coverage.cjs +142 -0
  85. package/dist/plugs/coverage.cjs.map +6 -0
  86. package/dist/plugs/coverage.mjs +117 -0
  87. package/dist/plugs/coverage.mjs.map +6 -0
  88. package/dist/plugs/debug.cjs +50 -0
  89. package/dist/plugs/debug.cjs.map +6 -0
  90. package/dist/plugs/debug.mjs +25 -0
  91. package/dist/plugs/debug.mjs.map +6 -0
  92. package/dist/plugs/esbuild/bundle-locals.cjs +51 -0
  93. package/dist/plugs/esbuild/bundle-locals.cjs.map +6 -0
  94. package/dist/plugs/esbuild/bundle-locals.mjs +26 -0
  95. package/dist/plugs/esbuild/bundle-locals.mjs.map +6 -0
  96. package/dist/plugs/esbuild/check-dependencies.cjs +140 -0
  97. package/dist/plugs/esbuild/check-dependencies.cjs.map +6 -0
  98. package/dist/plugs/esbuild/check-dependencies.mjs +115 -0
  99. package/dist/plugs/esbuild/check-dependencies.mjs.map +6 -0
  100. package/dist/plugs/esbuild/fix-extensions.cjs +91 -0
  101. package/dist/plugs/esbuild/fix-extensions.cjs.map +6 -0
  102. package/dist/plugs/esbuild/fix-extensions.mjs +60 -0
  103. package/dist/plugs/esbuild/fix-extensions.mjs.map +6 -0
  104. package/dist/plugs/esbuild.cjs +109 -0
  105. package/dist/plugs/esbuild.cjs.map +6 -0
  106. package/dist/plugs/esbuild.mjs +83 -0
  107. package/dist/plugs/esbuild.mjs.map +6 -0
  108. package/dist/plugs/eslint/runner.cjs +91 -0
  109. package/dist/plugs/eslint/runner.cjs.map +6 -0
  110. package/dist/plugs/eslint/runner.mjs +68 -0
  111. package/dist/plugs/eslint/runner.mjs.map +6 -0
  112. package/dist/plugs/exec.cjs +128 -0
  113. package/dist/plugs/exec.cjs.map +6 -0
  114. package/dist/plugs/exec.mjs +96 -0
  115. package/dist/plugs/exec.mjs.map +6 -0
  116. package/dist/plugs/filter.cjs +59 -0
  117. package/dist/plugs/filter.cjs.map +6 -0
  118. package/dist/plugs/filter.mjs +34 -0
  119. package/dist/plugs/filter.mjs.map +6 -0
  120. package/dist/plugs/mocha/reporter.cjs +140 -0
  121. package/dist/plugs/mocha/reporter.cjs.map +6 -0
  122. package/dist/plugs/mocha/reporter.mjs +107 -0
  123. package/dist/plugs/mocha/reporter.mjs.map +6 -0
  124. package/dist/plugs/mocha/runner.cjs +73 -0
  125. package/dist/plugs/mocha/runner.cjs.map +6 -0
  126. package/dist/plugs/mocha/runner.mjs +44 -0
  127. package/dist/plugs/mocha/runner.mjs.map +6 -0
  128. package/dist/plugs/tsc/compiler.cjs +74 -0
  129. package/dist/plugs/tsc/compiler.cjs.map +6 -0
  130. package/dist/plugs/tsc/compiler.mjs +43 -0
  131. package/dist/plugs/tsc/compiler.mjs.map +6 -0
  132. package/dist/plugs/tsc/options.cjs +82 -0
  133. package/dist/plugs/tsc/options.cjs.map +6 -0
  134. package/dist/plugs/tsc/options.mjs +51 -0
  135. package/dist/plugs/tsc/options.mjs.map +6 -0
  136. package/dist/plugs/tsc/report.cjs +90 -0
  137. package/dist/plugs/tsc/report.cjs.map +6 -0
  138. package/dist/plugs/tsc/report.mjs +59 -0
  139. package/dist/plugs/tsc/report.mjs.map +6 -0
  140. package/dist/plugs/tsc/runner.cjs +101 -0
  141. package/dist/plugs/tsc/runner.cjs.map +6 -0
  142. package/dist/plugs/tsc/runner.mjs +72 -0
  143. package/dist/plugs/tsc/runner.mjs.map +6 -0
  144. package/dist/plugs.cjs +31 -0
  145. package/dist/plugs.cjs.map +6 -0
  146. package/dist/plugs.mjs +13 -0
  147. package/dist/plugs.mjs.map +6 -0
  148. package/dist/run.cjs +95 -0
  149. package/dist/run.cjs.map +6 -0
  150. package/dist/run.mjs +70 -0
  151. package/dist/run.mjs.map +6 -0
  152. package/dist/task.cjs +39 -0
  153. package/dist/task.cjs.map +6 -0
  154. package/dist/task.mjs +14 -0
  155. package/dist/task.mjs.map +6 -0
  156. package/dist/utils/asyncfs.cjs +143 -0
  157. package/dist/utils/asyncfs.cjs.map +6 -0
  158. package/dist/utils/asyncfs.mjs +83 -0
  159. package/dist/utils/asyncfs.mjs.map +6 -0
  160. package/dist/utils/caller.cjs +59 -0
  161. package/dist/utils/caller.cjs.map +6 -0
  162. package/dist/utils/caller.mjs +34 -0
  163. package/dist/utils/caller.mjs.map +6 -0
  164. package/dist/utils/match.cjs +69 -0
  165. package/dist/utils/match.cjs.map +6 -0
  166. package/dist/utils/match.mjs +38 -0
  167. package/dist/utils/match.mjs.map +6 -0
  168. package/dist/utils/options.cjs +41 -0
  169. package/dist/utils/options.cjs.map +6 -0
  170. package/dist/utils/options.mjs +16 -0
  171. package/dist/utils/options.mjs.map +6 -0
  172. package/dist/utils/walk.cjs +104 -0
  173. package/dist/utils/walk.cjs.map +6 -0
  174. package/dist/utils/walk.mjs +79 -0
  175. package/dist/utils/walk.mjs.map +6 -0
  176. package/extra/cli.mjs +212 -0
  177. package/extra/ts-loader.mjs +214 -0
  178. package/extra/webassembly.d.ts +11 -0
  179. package/package.json +57 -0
  180. package/src/assert.ts +47 -0
  181. package/src/async.ts +50 -0
  182. package/src/files.ts +129 -0
  183. package/src/fork.ts +263 -0
  184. package/src/helpers.ts +145 -0
  185. package/src/index.ts +20 -0
  186. package/src/log/colors.ts +119 -0
  187. package/src/log/emit.ts +125 -0
  188. package/src/log/levels.ts +65 -0
  189. package/src/log/logger.ts +171 -0
  190. package/src/log/options.ts +199 -0
  191. package/src/log/report.ts +433 -0
  192. package/src/log/spinner.ts +70 -0
  193. package/src/log.ts +68 -0
  194. package/src/paths.ts +213 -0
  195. package/src/pipe.ts +231 -0
  196. package/src/plugs/copy.ts +113 -0
  197. package/src/plugs/coverage/analysis.ts +395 -0
  198. package/src/plugs/coverage/report.ts +337 -0
  199. package/src/plugs/coverage.ts +194 -0
  200. package/src/plugs/debug.ts +35 -0
  201. package/src/plugs/esbuild/bundle-locals.ts +33 -0
  202. package/src/plugs/esbuild/check-dependencies.ts +158 -0
  203. package/src/plugs/esbuild/fix-extensions.ts +108 -0
  204. package/src/plugs/esbuild.ts +128 -0
  205. package/src/plugs/eslint/runner.ts +112 -0
  206. package/src/plugs/exec.ts +215 -0
  207. package/src/plugs/filter.ts +56 -0
  208. package/src/plugs/mocha/reporter.ts +152 -0
  209. package/src/plugs/mocha/runner.ts +77 -0
  210. package/src/plugs/tsc/compiler.ts +66 -0
  211. package/src/plugs/tsc/options.ts +97 -0
  212. package/src/plugs/tsc/report.ts +74 -0
  213. package/src/plugs/tsc/runner.ts +100 -0
  214. package/src/plugs.ts +33 -0
  215. package/src/run.ts +160 -0
  216. package/src/task.ts +26 -0
  217. package/src/utils/asyncfs.ts +82 -0
  218. package/src/utils/caller.ts +45 -0
  219. package/src/utils/match.ts +286 -0
  220. package/src/utils/options.ts +22 -0
  221. package/src/utils/walk.ts +136 -0
  222. package/types/assert.d.ts +18 -0
  223. package/types/async.d.ts +20 -0
  224. package/types/build.d.ts +56 -0
  225. package/types/files.d.ts +44 -0
  226. package/types/fork.d.ts +57 -0
  227. package/types/helpers.d.ts +49 -0
  228. package/types/index.d.ts +14 -0
  229. package/types/log/colors.d.ts +25 -0
  230. package/types/log/emit.d.ts +14 -0
  231. package/types/log/levels.d.ts +52 -0
  232. package/types/log/logger.d.ts +31 -0
  233. package/types/log/options.d.ts +40 -0
  234. package/types/log/report.d.ts +64 -0
  235. package/types/log/spinner.d.ts +2 -0
  236. package/types/log.d.ts +10 -0
  237. package/types/paths.d.ts +76 -0
  238. package/types/pipe.d.ts +152 -0
  239. package/types/plugs/copy.d.ts +27 -0
  240. package/types/plugs/coverage/analysis.d.ts +104 -0
  241. package/types/plugs/coverage/report.d.ts +53 -0
  242. package/types/plugs/coverage.d.ts +46 -0
  243. package/types/plugs/debug.d.ts +14 -0
  244. package/types/plugs/esbuild/bundle-locals.d.ts +6 -0
  245. package/types/plugs/esbuild/check-dependencies.d.ts +12 -0
  246. package/types/plugs/esbuild/fix-extensions.d.ts +29 -0
  247. package/types/plugs/esbuild.d.ts +24 -0
  248. package/types/plugs/eslint/runner.d.ts +22 -0
  249. package/types/plugs/exec.d.ts +90 -0
  250. package/types/plugs/filter.d.ts +23 -0
  251. package/types/plugs/mocha/reporter.d.ts +8 -0
  252. package/types/plugs/mocha/runner.d.ts +34 -0
  253. package/types/plugs/tsc/compiler.d.ts +24 -0
  254. package/types/plugs/tsc/options.d.ts +8 -0
  255. package/types/plugs/tsc/report.d.ts +5 -0
  256. package/types/plugs/tsc/runner.d.ts +13 -0
  257. package/types/plugs.d.ts +16 -0
  258. package/types/run.d.ts +89 -0
  259. package/types/task.d.ts +15 -0
  260. package/types/utils/asyncfs.d.ts +37 -0
  261. package/types/utils/caller.d.ts +7 -0
  262. package/types/utils/match.d.ts +216 -0
  263. package/types/utils/options.d.ts +15 -0
  264. package/types/utils/walk.d.ts +28 -0
@@ -0,0 +1,215 @@
1
+ import path from 'node:path'
2
+ import reaadline from 'node:readline'
3
+
4
+ import { spawn, SpawnOptions } from 'node:child_process'
5
+ import { assert } from '../assert.js'
6
+ import { currentRun } from '../async.js'
7
+ import { Files } from '../files.js'
8
+ import { $p, logOptions } from '../log.js'
9
+ import { AbsolutePath, getCurrentWorkingDirectory, isDirectory } from '../paths.js'
10
+ import { install, Plug } from '../pipe.js'
11
+ import { Run } from '../run.js'
12
+ import { parseOptions, ParseOptions } from '../utils/options.js'
13
+
14
+ /** Options for executing scripts */
15
+ export interface ExecOptions {
16
+ /** Extra environment variables, or overrides for existing ones */
17
+ env?: Record<string, any>,
18
+ /** Whether to run in the context of a _shell_ or not */
19
+ shell?: string | boolean,
20
+ /**
21
+ * The current working directory of the process to execute.
22
+ *
23
+ * Defaults to the current {@link Files.directory | Files' directory} when
24
+ * used as a {@link Plug} or `process.cwd()` when used from {@link exec}.
25
+ */
26
+ cwd?: string
27
+ /**
28
+ * When used as a {@link Plug}, the {@link Files} will be appended to the
29
+ * current arguments as _relative_ files (default) or _absolute_ (if false).
30
+ */
31
+ relativePaths?: boolean
32
+ }
33
+
34
+ /**
35
+ * Execute a shell command, adding to its _arguments_ the list of files from
36
+ * the current pipe (much like `xargs` does on Unix systems).
37
+ *
38
+ * This {@link Plug} returns the same {@link Files} it was given, so that
39
+ * executing a shell command doesn't interrupt a {@link Pipe}.
40
+ *
41
+ * For example:
42
+ *
43
+ * ```
44
+ * import { find, exec } from '@plugjs/plugjs'
45
+ *
46
+ * export default build({
47
+ * async runme() {
48
+ * find('*.ts', { directory: 'src' })
49
+ * .plug(new Exec('chmod', '755' }))
50
+ * .plug(new Exec('chown root:root', { shell: true }))
51
+ * },
52
+ * })
53
+ * ```
54
+ */
55
+ export class Exec implements Plug<Files> {
56
+ private readonly _cmd: string
57
+ private readonly _args: readonly string[]
58
+ private readonly _options: ExecOptions
59
+
60
+ constructor(cmd: string, ...args: ParseOptions<ExecOptions>) {
61
+ const { params, options } = parseOptions(args, {})
62
+ this._cmd = cmd
63
+ this._args = params
64
+ this._options = options
65
+ }
66
+
67
+ async pipe(files: Files, run: Run): Promise<Files> {
68
+ const { relativePaths = true, ...options } = this._options
69
+
70
+ if (! options.cwd) options.cwd = files.directory
71
+
72
+ // What to use as extra arguments? Relative or absolute paths?
73
+ const params = [ ...(relativePaths ? files : files.absolutePaths() ) ]
74
+
75
+ // In case of shell usage, each extra parameter (file) gets quoted!
76
+ if (options.shell) params.forEach((s, i, a) => a[i] = JSON.stringify(s))
77
+
78
+ // Run our child
79
+ await spawnChild(this._cmd, [ ...this._args, ...params ], options, run)
80
+
81
+ // Return our files
82
+ return files
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Execute a command and await for its result from within a task.
88
+ *
89
+ * For example:
90
+ *
91
+ * ```
92
+ * import { exec } from '@plugjs/plugjs'
93
+ *
94
+ * export default build({
95
+ * async runme() {
96
+ * await exec('ls', '-la', '/')
97
+ * },
98
+ * })
99
+ * ```
100
+ */
101
+
102
+ export function exec(cmd: string, ...args: ParseOptions<ExecOptions>): Promise<void> {
103
+ const run = currentRun()
104
+ assert(run, 'Unable to execute commands outside a running task')
105
+
106
+ const { params, options } = parseOptions(args, {})
107
+ return spawnChild(cmd, params, options, run)
108
+ }
109
+
110
+
111
+ /* ========================================================================== *
112
+ * INSTALLATION *
113
+ * ========================================================================== */
114
+
115
+ install('exec', Exec)
116
+
117
+ declare module '../pipe.js' {
118
+ export interface Pipe {
119
+ /**
120
+ * Execute a shell command, adding to its _arguments_ the list of files
121
+ * from the current pipe (much like `xargs` does on Unix systems).
122
+ *
123
+ * For example:
124
+ *
125
+ * ```
126
+ * import { find, exec } from '@plugjs/plugjs'
127
+ *
128
+ * export default build({
129
+ * async runme() {
130
+ * find('*.ts', { directory: 'src' })
131
+ * .exec('chmod', '755' })
132
+ * .exec('chown root:root', { shell: true })
133
+ * },
134
+ * })
135
+ * ```
136
+ */
137
+ exec: PipeExtension<typeof Exec>
138
+ }
139
+ }
140
+
141
+ /* ========================================================================== *
142
+ * INTERNALS *
143
+ * ========================================================================== */
144
+
145
+ async function spawnChild(
146
+ cmd: string,
147
+ args: readonly string[],
148
+ options: ExecOptions = {},
149
+ run: Run,
150
+ ): Promise<void> {
151
+ const {
152
+ env = {}, // default empty environment
153
+ shell = false, // by default do not use a shell
154
+ cwd = undefined, // by default use "process.cwd()"
155
+ ...extraOptions
156
+ } = options
157
+
158
+ const childCwd = cwd ? run.resolve(cwd) : getCurrentWorkingDirectory()
159
+ assert(isDirectory(childCwd), `Current working directory ${$p(childCwd)} does not exist`)
160
+
161
+ // Figure out the PATH environment variable
162
+ const childPaths: AbsolutePath[] = []
163
+
164
+ // The `.../node_modules/.bin` path relative to the current working dir */
165
+ const baseNodePath = run.resolve('@node_modules', '.bin')
166
+ if (isDirectory(baseNodePath)) childPaths.push(baseNodePath)
167
+
168
+ // The `.../node_bodules/.bin` path relative to the buildDir */
169
+ const buildNodePath = run.resolve('./node_modules', '.bin')
170
+ if (isDirectory(buildNodePath)) childPaths.push(buildNodePath)
171
+
172
+ // Any other paths either from `process.env` or `env` (which overrides it)
173
+ const extraPath = env.PATH || process.env.PATH
174
+ if (extraPath) childPaths.push(extraPath)
175
+
176
+ // Build our environment variables record
177
+ const PATH = childPaths.join(path.delimiter)
178
+ const __LOG_OPTIONS = JSON.stringify(logOptions.fork(run.taskName))
179
+ const childEnv = { ...process.env, ...env, PATH, __LOG_OPTIONS }
180
+
181
+ // Prepare the options for calling `spawn`
182
+ const childOptions: SpawnOptions = {
183
+ ...extraOptions,
184
+ stdio: [ 'ignore', 'pipe', 'pipe' ],
185
+ cwd: childCwd,
186
+ env: childEnv,
187
+ shell,
188
+ }
189
+
190
+ // Spawn our subprocess and monitor its stdout/stderr
191
+ run.log.info('Executing', [ cmd, ...args ])
192
+ run.log.info('Execution options', childOptions)
193
+ const child = spawn(cmd, args, childOptions)
194
+
195
+ if (child.stdout) {
196
+ const out = reaadline.createInterface(child.stdout)
197
+ out.on('line', (line) => line ? run.log.notice(line) : run.log.notice('\u00a0'))
198
+ }
199
+
200
+ if (child.stderr) {
201
+ const err = reaadline.createInterface(child.stderr)
202
+ err.on('line', (line) => line ? run.log.warn(line) : run.log.warn('\u00a0'))
203
+ }
204
+
205
+ // Return our promise from the spawn events
206
+ return new Promise<void>((resolve, reject) => {
207
+ child.on('error', (error) => reject(error))
208
+ child.on('exit', (code, signal) => {
209
+ if (code === 0) return resolve()
210
+ if (signal) return reject(new Error(`Child process exited with signal ${signal}`))
211
+ if (code) return reject(new Error(`Child process exited with code ${code}`))
212
+ reject(new Error('Child process failed for an unknown reason'))
213
+ })
214
+ })
215
+ }
@@ -0,0 +1,56 @@
1
+ import { Files } from '../files.js'
2
+ import { log } from '../log.js'
3
+ import { resolveRelativeChildPath } from '../paths.js'
4
+ import { install, Plug } from '../pipe.js'
5
+ import { Run } from '../run.js'
6
+ import { match, MatchOptions } from '../utils/match.js'
7
+ import { ParseOptions, parseOptions } from '../utils/options.js'
8
+
9
+ /** Options for filtering {@link Files}. */
10
+ export interface FilterOptions extends MatchOptions {
11
+ /** The base directory for filtering, and relativising the resulting files. */
12
+ directory?: string
13
+ }
14
+
15
+ /** Filter the current {@link Files} using globs. */
16
+ export class Filter implements Plug<Files> {
17
+ private readonly _globs: readonly [ string, ...readonly string[] ]
18
+ private readonly _options: FilterOptions
19
+
20
+ constructor(glob: string, ...args: ParseOptions<FilterOptions>) {
21
+ const { params, options } = parseOptions(args, {})
22
+ this._globs = [ glob, ...params ]
23
+ this._options = options
24
+ }
25
+
26
+ pipe(files: Files, run: Run): Files {
27
+ const { directory, ...options } = this._options
28
+
29
+ const builder = run.files(directory || files.directory)
30
+ const matcher = match(this._globs, options)
31
+
32
+ for (const file of files.absolutePaths()) {
33
+ const relative = resolveRelativeChildPath(builder.directory, file)
34
+ if (relative && matcher(relative)) builder.add(relative)
35
+ }
36
+
37
+ const result = builder.build()
38
+ const discarded = files.length - result.length
39
+ log.debug('Filtered', result.length, 'files (discarded', discarded, 'files)')
40
+
41
+ return result
42
+ }
43
+ }
44
+
45
+ /* ========================================================================== *
46
+ * INSTALLATION *
47
+ * ========================================================================== */
48
+
49
+ install('filter', Filter)
50
+
51
+ declare module '../pipe.js' {
52
+ export interface Pipe {
53
+ /** Filter the current {@link Files} using globs. */
54
+ filter: PipeExtension<typeof Filter>
55
+ }
56
+ }
@@ -0,0 +1,152 @@
1
+ import RealMocha from 'mocha' // Mocha types pollute the global scope!
2
+
3
+ import { AssertionError } from 'assert'
4
+ import { diffJson } from 'diff'
5
+ import { $blu, $grn, $gry, $ms, $red, $wht, $ylw, ERROR, Logger, NOTICE, WARN } from '../../log.js'
6
+
7
+ const _pending = '\u22EF' // middle ellipsis
8
+ const _success = '\u2714' // heavy check mark
9
+ const _failure = '\u2718' // heavy ballot x
10
+ const _details = '\u21B3' // downwards arrow with tip rightwards
11
+
12
+ /* ========================================================================== *
13
+ * LOGGER / REPORTER *
14
+ * ========================================================================== */
15
+
16
+ /** Symbol to inject `Logger` in reporter options */
17
+ export const logSymbol = Symbol()
18
+ /** Symbol to inject `Run` in reporter options */
19
+ export const runSymbol = Symbol()
20
+
21
+ export class PlugReporter extends RealMocha.reporters.Base {
22
+ constructor(runner: RealMocha.Runner, options: RealMocha.MochaOptions) {
23
+ super(runner, options)
24
+
25
+ const showDiff = !! options.diff
26
+ const log = options.reporterOptions[logSymbol] as Logger
27
+ const failures: RealMocha.Test[] = []
28
+ const rootSuite = runner.suite
29
+
30
+ // Enter a suite (increase indent)
31
+ runner.on('suite', (suite) => {
32
+ if (suite === rootSuite) return
33
+ log.notice('')
34
+ log.enter(NOTICE, `${$wht(suite.title)}`)
35
+ })
36
+
37
+ // Leave a suite (decrease indent)
38
+ runner.on('suite end', () => {
39
+ log.leave()
40
+ })
41
+
42
+ // Enter a test (increase indent)
43
+ runner.on('test', (test) => {
44
+ log.enter(NOTICE, `${$blu(_pending)} ${test.title}`)
45
+ })
46
+
47
+ // Leave a test (handle warning/failures and decrease indent)
48
+ runner.on('test end', (test) => {
49
+ // TODO: slow!!!
50
+ if (test.isPassed()) {
51
+ log.leave(NOTICE, `${$grn(_success)} ${test.title}`)
52
+ } else if (test.isPending()) {
53
+ const tag = $gry('[') + $ylw('skipped') + $gry(']')
54
+ log.leave(WARN, `${$ylw(_pending)} ${test.title} ${tag}`)
55
+ } else if (test.isFailed()) {
56
+ const number = failures.push(test)
57
+ const tag = $gry('[') + $red('failed') + $gry('] [') + $red(number) + $gry(']')
58
+ log.leave(ERROR, `${$red(_failure)} ${test.title} ${tag}`)
59
+ }
60
+ })
61
+
62
+ // Mocha finished running, dump our report
63
+ runner.once('end', () => {
64
+ try {
65
+ // Each failure gets dumped individually
66
+ for (let i = 0; i < failures.length; i ++) {
67
+ log.notice('')
68
+ const failure = failures[i]
69
+
70
+ // The titles (from the suite, up to the test)
71
+ const titles = [ failure.title ]
72
+ for (let parent = failure.parent; parent; parent = parent?.parent) {
73
+ if (parent.parent) titles.unshift(parent.title)
74
+ }
75
+
76
+ // Log out our titles (one per line, indented)
77
+ log.error(`${$gry('Failure [')}${$red(i + 1)}${$gry(']')}`)
78
+ titles.forEach((title, indent) => {
79
+ log.error(` ${''.padStart(indent * 4)}${$gry(_details)} ${$wht(title)}`)
80
+ })
81
+
82
+ // If we have an error, luckily this is an `Error` instance
83
+ if (failure.err) {
84
+ const message = `${failure.err}` // this is the message, can be multiple lines
85
+ const messageOrStack = failure.err.stack || `${failure.err}` // maybe a stack?
86
+ const messageIndex = messageOrStack.indexOf(message)
87
+
88
+ // Subtrack the message from the stack
89
+ const stack =
90
+ messageOrStack === message ? '' :
91
+ messageIndex < 0 ? messageOrStack :
92
+ messageOrStack.substring(messageIndex + message.length)
93
+
94
+ // Split and clean up stack lines
95
+ const stackLines = stack.split('\n')
96
+ .map((line) => line.trim())
97
+ .filter((line) => !! line)
98
+
99
+ // Output the message
100
+ log.enter(ERROR, '')
101
+ log.error($red(message))
102
+
103
+ // Should we diff?
104
+ if (showDiff && ('actual' in failure.err) && ('expected' in failure.err)) {
105
+ const err = failure.err as AssertionError
106
+ const actual =
107
+ err.actual === undefined ? '[undefined]' :
108
+ err.actual === null ? '[null]' :
109
+ err.actual
110
+
111
+ const expected =
112
+ err.expected === undefined ? '[undefined]' :
113
+ err.expected === null ? '[null]' :
114
+ err.expected
115
+
116
+ const changes = diffJson(actual as any, expected as any)
117
+
118
+ const diff = changes.map((change): string => {
119
+ if (change.removed) return $red(change.value)
120
+ if (change.added) return $grn(change.value)
121
+ return $gry(change.value)
122
+ }).join('')
123
+
124
+ log.enter(ERROR, `${$gry('diff')} ${$grn('expected')} ${$gry('/')} ${$red('actual')}`)
125
+ log.error(diff)
126
+ log.leave()
127
+ }
128
+
129
+ // Dump our stack trace and leave
130
+ stackLines.forEach((line) => log.error(line))
131
+ log.leave()
132
+ }
133
+ }
134
+
135
+ // If we have some statistics, then let's dump them out in pretty colors
136
+ if (runner.stats) {
137
+ log.notice('')
138
+ const { passes, pending, failures, duration = 0 } = runner.stats
139
+ const fmt = (n: number): string => n === 1 ? `${n} test` : `${n} tests`
140
+ if (passes) log.notice($grn(fmt(passes)), 'passing', $ms(duration))
141
+ if (pending) log.warn($ylw(fmt(pending)), 'pending')
142
+ if (failures) log.error($red(fmt(failures)), 'pending')
143
+ }
144
+
145
+ // Done...
146
+ log.notice('')
147
+ } catch (error) {
148
+ log.error('Error rendering Mocha report', error)
149
+ }
150
+ })
151
+ }
152
+ }
@@ -0,0 +1,77 @@
1
+ import RealMocha from 'mocha' // Mocha types pollute the global scope!
2
+
3
+ import { failure } from '../../assert.js'
4
+ import { Files } from '../../files.js'
5
+ import { $wht, NOTICE } from '../../log.js'
6
+ import { Plug } from '../../pipe.js'
7
+ import { Run } from '../../run.js'
8
+ import { logSymbol, PlugReporter, runSymbol } from './reporter.js'
9
+
10
+ /** Options to construct our {@link Mocha} plug. */
11
+ export interface MochaOptions {
12
+ /** Specify the directory where coverage data will be saved */
13
+ coverageDir?: string,
14
+ /** Bail after first test failure? */
15
+ bail?: boolean,
16
+ /** Show diff on failure? */
17
+ diff?: boolean,
18
+ /** Report tests without running them? */
19
+ dryRun?: boolean,
20
+ /** Tests marked `only` fail the suite? */
21
+ forbidOnly?: boolean,
22
+ /** Pending tests fail the suite? */
23
+ forbidPending?: false,
24
+ /** Reporter name. */
25
+ reporter?: string
26
+ /** Options for the reporter */
27
+ reporterOptions?: Record<string, any>,
28
+ /** Number of times to retry failed tests. */
29
+ retries?: number,
30
+ /** Slow threshold value. */
31
+ slow?: number,
32
+ /** Timeout threshold value. */
33
+ timeout?: number,
34
+ }
35
+
36
+ /** Writes some info about the current {@link Files} being passed around. */
37
+ export default class Mocha implements Plug<undefined> {
38
+ constructor(private readonly _options: MochaOptions = {}) {}
39
+
40
+ async pipe(files: Files, run: Run): Promise<undefined> {
41
+ // Enter log here, so that log messages called when loading files get
42
+ // properly indented by our logger
43
+ run.log.notice('') // empty line
44
+ run.log.enter(NOTICE, $wht('Starting Mocha'))
45
+
46
+ // Create the mocha runner
47
+ const mocha = new RealMocha({
48
+ diff: true, // by defaut enable diffs
49
+ reporter: PlugReporter, // default to our reporter
50
+ ...this._options, // override defaults with all other options
51
+ reporterOptions: {
52
+ ...this._options.reporterOptions,
53
+ [logSymbol]: run.log, // always force a log
54
+ [runSymbol]: run, // always force a run
55
+ },
56
+ allowUncaught: false, // never allow uncaught exceptions
57
+ delay: false, // never delay running
58
+ })
59
+
60
+ // Tell mocha about all our files
61
+ for (const file of files.absolutePaths()) mocha.addFile(file)
62
+
63
+ await mocha.loadFilesAsync()
64
+
65
+ // Run mocha!
66
+ return new Promise((resolve, reject) => {
67
+ try {
68
+ mocha.run((failures) => {
69
+ if (failures) reject(failure())
70
+ resolve(undefined)
71
+ })
72
+ } catch (error) {
73
+ reject(error)
74
+ }
75
+ })
76
+ }
77
+ }
@@ -0,0 +1,66 @@
1
+ import ts from 'typescript' // TypeScript does NOT support ESM modules
2
+
3
+ import { AbsolutePath, resolveAbsolutePath } from '../../paths.js'
4
+
5
+ export class TypeScriptHost
6
+ implements ts.CompilerHost {
7
+ constructor(directory: AbsolutePath)
8
+ constructor(private readonly _directory: AbsolutePath) {}
9
+
10
+ /* ======================================================================== */
11
+
12
+ /** Get a source file parsing one of our virtual files */
13
+ getSourceFile(
14
+ fileName: string,
15
+ languageVersion: ts.ScriptTarget,
16
+ ): ts.SourceFile | undefined {
17
+ const code = this.readFile(fileName)
18
+ return code ? ts.createSourceFile(fileName, code, languageVersion) : void 0
19
+ }
20
+
21
+ /** Never write any files */
22
+ writeFile(fileName: string): void {
23
+ throw new Error(`Cowardly refusing to write "${fileName}"`)
24
+ }
25
+
26
+ /** Get the default library associated with the given options */
27
+ getDefaultLibFileName(options: ts.CompilerOptions): string {
28
+ return ts.getDefaultLibFilePath(options)
29
+ }
30
+
31
+ /** Check for filesystem case sensitivity */
32
+ useCaseSensitiveFileNames(): boolean {
33
+ return ts.sys.useCaseSensitiveFileNames
34
+ }
35
+
36
+ /** Check for the existence of a given file */
37
+ fileExists(fileName: string): boolean {
38
+ return ts.sys.fileExists(resolveAbsolutePath(this.getCurrentDirectory(), fileName))
39
+ }
40
+
41
+ /** Read the file if it exists, otherwise return undefined */
42
+ readFile(fileName: string): string | undefined {
43
+ return ts.sys.readFile(resolveAbsolutePath(this.getCurrentDirectory(), fileName))
44
+ }
45
+
46
+ /** Return the current working directory */
47
+ getCurrentDirectory(): AbsolutePath {
48
+ return this._directory
49
+ }
50
+
51
+ /** Return the canonical name for the specified file */
52
+ getCanonicalFileName(fileName: string): string {
53
+ if (ts.sys.useCaseSensitiveFileNames) return fileName
54
+
55
+ // Lifted from TypeScript sources
56
+ const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_. ]+/g
57
+ return fileNameLowerCaseRegExp.test(fileName) ?
58
+ fileName.replace(fileNameLowerCaseRegExp, (s) => s.toLowerCase()) :
59
+ fileName
60
+ }
61
+
62
+ /** Return the new line sequence used by this platform */
63
+ getNewLine(): string {
64
+ return ts.sys.newLine
65
+ }
66
+ }
@@ -0,0 +1,97 @@
1
+ import ts from 'typescript' // TypeScript does NOT support ESM modules
2
+
3
+ import { AbsolutePath, getAbsoluteParent, resolveAbsolutePath } from '../../paths.js'
4
+ import { readFile } from '../../utils/asyncfs.js'
5
+
6
+ /* ========================================================================== */
7
+
8
+ export type CompilerOptionsAndDiagnostics = {
9
+ options: ts.CompilerOptions,
10
+ errors: readonly ts.Diagnostic[],
11
+ }
12
+
13
+ /* ========================================================================== */
14
+
15
+ function mergeResults(
16
+ base: CompilerOptionsAndDiagnostics,
17
+ override: CompilerOptionsAndDiagnostics,
18
+ ): CompilerOptionsAndDiagnostics {
19
+ return {
20
+ options: { ...base.options, ...override.options },
21
+ errors: [ ...base.errors, ...override.errors ],
22
+ }
23
+ }
24
+
25
+ /* ========================================================================== */
26
+
27
+ async function loadOptions(
28
+ file: AbsolutePath,
29
+ stack: AbsolutePath[] = [ file ],
30
+ ): Promise<CompilerOptionsAndDiagnostics> {
31
+ const dir = getAbsoluteParent(file)
32
+
33
+ // Load up our config file and convert is wicked JSON
34
+ const data = await readFile(file, 'utf-8')
35
+ const { config, error } = ts.parseConfigFileTextToJson(file, data)
36
+ if (error) return { options: {}, errors: [ error ] }
37
+
38
+ // Parse up the configuration file as options
39
+ const { compilerOptions = {}, extends: extendsPath } = config
40
+ const result = ts.convertCompilerOptionsFromJson(compilerOptions, dir, file)
41
+ if (result.errors.length) return result
42
+
43
+ // If we don't extend, we can return our result
44
+ if (!extendsPath) return result
45
+
46
+ // Resolve the name of the file this config extends
47
+ const ext = resolveAbsolutePath(dir, extendsPath)
48
+
49
+ // Triple check that we are not recursively importing this file
50
+ if (stack.includes(ext)) {
51
+ return { options: {}, errors: [ {
52
+ messageText: `Circularity detected extending from "${ext}"`,
53
+ category: ts.DiagnosticCategory.Error,
54
+ code: 18000, // copied from typescript internals...
55
+ file: ts.createSourceFile(file, data, ts.ScriptTarget.JSON, false, ts.ScriptKind.JSON),
56
+ start: undefined,
57
+ length: undefined,
58
+ } ] }
59
+ }
60
+
61
+ // Push our file in the stack and load recursively
62
+ return mergeResults(await loadOptions(ext, [ ...stack, ext ]), result)
63
+ }
64
+
65
+ /* ========================================================================== */
66
+
67
+ export async function getCompilerOptions(
68
+ file?: AbsolutePath,
69
+ ): Promise<CompilerOptionsAndDiagnostics>
70
+
71
+ export async function getCompilerOptions(
72
+ file: AbsolutePath | undefined,
73
+ overrides: ts.CompilerOptions,
74
+ overridesFile: AbsolutePath,
75
+ ): Promise<CompilerOptionsAndDiagnostics>
76
+
77
+ /** Load compiler options from a JSON file, and merge in the overrides */
78
+ export async function getCompilerOptions(
79
+ file?: AbsolutePath,
80
+ ...override: [ ts.CompilerOptions, AbsolutePath ] | []
81
+ ): Promise<CompilerOptionsAndDiagnostics> {
82
+ let result: CompilerOptionsAndDiagnostics = { options: ts.getDefaultCompilerOptions(), errors: [] }
83
+
84
+ // If we have a file to parse, load it, otherwise try "tsconfig.json"
85
+ if (file) result = mergeResults(result, await loadOptions(file))
86
+
87
+ // If we have overrides, merge them
88
+ if (override.length) {
89
+ const [ overrides, overridesFile ] = override
90
+ const overridesDir = getAbsoluteParent(overridesFile)
91
+ const options = ts.convertCompilerOptionsFromJson(overrides, overridesDir, overridesFile)
92
+ result = mergeResults(result, options)
93
+ }
94
+
95
+ // Return all we have
96
+ return result
97
+ }