adapt-migrations 1.2.0 → 1.3.0

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.
package/lib/Task.js CHANGED
@@ -1,381 +1,386 @@
1
- import globs from 'globs'
2
- import path from 'path'
3
- import fs from 'fs-extra'
4
- import chalk from 'chalk'
5
- import TaskContext from './TaskContext.js'
6
- import Journal from '../lib/Journal.js'
7
- import { exec } from 'child_process'
8
-
9
- function makeTable(items, columnNames) {
10
- // calculate the max column widths for the items
11
- const colWidths = items.reduce((colWidths, item) => {
12
- Object.entries(item).forEach(([colName, value]) => {
13
- colWidths[colName] = Math.max(colWidths[colName] ?? 0, value ? String(value).length : 0)
14
- if (!colWidths[colName]) return
15
- // Include column header if data has value
16
- colWidths[colName] = Math.max(colWidths[colName], colName.length)
17
- })
18
- return colWidths
19
- }, {})
20
- // produce a well spaced column names header
21
- const header = columnNames.map(colName => {
22
- if (!colWidths[colName]) return ''
23
- return chalk.cyan(String(colName).padEnd(colWidths[colName] + 1, ' '))
24
- }).join('')
25
- // produce a well spaced data table
26
- const body = items.map(item => {
27
- return columnNames.map(colName => {
28
- if (!colWidths[colName]) return ''
29
- return String(item[colName]).padEnd(colWidths[colName] + 1, ' ')
30
- }).join('')
31
- }).join('\n')
32
- return `${header}\n${body}`
33
- }
34
-
35
- function prettyPrintJournal(journal, logger) {
36
- const lines = []
37
- // collect all of the appropriate information about the change
38
- for (const entry of journal.entries) {
39
- const type = (entry.keys[0] === 'fromPlugins')
40
- ? 'plugin'
41
- : entry._type
42
- const name = entry._name
43
- const id = entry._id
44
- const action = entry.method
45
- const property = entry.keys.slice(2).join('.')
46
- const value = JSON.stringify(entry.value)
47
- const prev = JSON.stringify(entry.previous)
48
- lines.push({ type, name, id, '#': lines.length, action, property, value, prev })
49
- }
50
- let lineIndex = 0
51
- const output = []
52
- while (lineIndex < lines.length) {
53
- // collect lines with identical subject name values
54
- const fromLineIndex = lineIndex
55
- let toLineIndex = lineIndex
56
- const line = lines[lineIndex]
57
- for (let i = lineIndex; i < lines.length; i++) {
58
- const nextLine = lines[i]
59
- if (nextLine.name !== line.name) break
60
- toLineIndex = i
61
- }
62
- // print a grouped summary of the above lines with a single common subject line
63
- const printLines = lines.slice(fromLineIndex, toLineIndex + 1)
64
- const subjectTable = makeTable([printLines[0]], ['type', 'name', 'id'])
65
- const propertyChangeTable = makeTable(printLines, ['#', 'action','property','value'])
66
- output.push(`${subjectTable}\n${propertyChangeTable}\n`)
67
- lineIndex = toLineIndex + 1
68
- }
69
- logger.info(`Summary of changes:\n\n${output.join('\n')}`)
70
- }
71
-
72
- export default class Task {
73
- constructor ({
74
- description = '',
75
- tests = [],
76
- steps = [],
77
- filePath = Task.currentFile,
78
- load = () => {}
79
- } = {}) {
80
- this.description = description
81
- this.tests = tests
82
- this.steps = steps
83
- this.load = load
84
- this.filePath = filePath
85
- if (!load) return // Assumed cloned
86
- Task.described.push(this)
87
- }
88
-
89
- clone () {
90
- return new Task({
91
- description: this.description,
92
- tests: this.tests,
93
- steps: this.steps,
94
- filePath: this.filePath,
95
- load: null
96
- })
97
- }
98
-
99
- getImmediateNextStepsOfType (stepIndex = 0, type) {
100
- const nextFilters = []
101
- const nextSteps = this.steps.slice(stepIndex)
102
- for (const step of nextSteps) {
103
- if (step.type === type) {
104
- nextFilters.push(step)
105
- continue
106
- }
107
- break
108
- }
109
- return nextFilters
110
- }
111
-
112
- getImmediateNextIndexOfType (stepIndex = 0, type) {
113
- const nextSteps = this.steps.slice(stepIndex)
114
- for (const nextStepIndex in nextSteps) {
115
- const step = nextSteps[nextStepIndex]
116
- if (step.type === type) {
117
- return parseInt(nextStepIndex) + stepIndex
118
- }
119
- }
120
- return -1
121
- }
122
-
123
- async run ({ journal, logger }) {
124
- logger.debug(`Task -- Running ${this.description}`)
125
- let shouldContinue = false
126
- const lastJournalEntryIndex = journal.lastEntryIndex
127
- this.context = new TaskContext({
128
- fromPlugins: journal.subject.fromPlugins,
129
- originalFromPlugins: journal.subject.originalFromPlugins,
130
- toPlugins: journal.subject.toPlugins,
131
- content: journal.subject.content,
132
- journal
133
- })
134
- Task.isRunning = true
135
- Task.stackUp(this)
136
- const wheres = this.getImmediateNextStepsOfType(0, 'where')
137
- let stepIndex = 0
138
- const stepCount = this.steps.length
139
- while (stepIndex < stepCount) {
140
- if (stepIndex >= wheres.length) {
141
- this.context.hasRun = true
142
- }
143
- const step = this.steps[stepIndex]
144
- try {
145
- shouldContinue = await step(this.context)
146
- } catch (err) {
147
- logger.error(`Task -- shouldContinue errored ${err}`)
148
- this.context.errors.push(err)
149
- this.context.hasErrored = true
150
- // Undo changes from this errored migrations
151
- journal.undoToIndex(lastJournalEntryIndex)
152
- stepIndex = this.getImmediateNextIndexOfType(stepIndex, 'error')
153
- shouldContinue = (stepIndex !== -1)
154
- if (shouldContinue) continue
155
- }
156
- if (shouldContinue) {
157
- stepIndex++
158
- continue
159
- }
160
- this.context.hasStopped = true
161
- break
162
- }
163
- this.isComplete = true
164
- // TODO: update current plugin version
165
-
166
- const result = this.context
167
- this.context = null
168
- Task.stackDown()
169
- Task.isRunning = false
170
- return result
171
- };
172
-
173
- async isApplicable ({ journal }) {
174
- Task.isRunning = true
175
- Task.stackUp(this)
176
- this.context = new TaskContext({
177
- fromPlugins: journal.subject.fromPlugins,
178
- originalFromPlugins: journal.subject.originalFromPlugins,
179
- toPlugins: journal.subject.toPlugins,
180
- content: journal.subject.content,
181
- journal
182
- })
183
- let shouldRun = true
184
- const wheres = this.getImmediateNextStepsOfType(0, 'where')
185
-
186
- for (const where of wheres) {
187
- this.isRunning = true
188
- const result = await where(this.context)
189
- this.isRunning = false
190
- if (result) continue
191
- shouldRun = false
192
- break
193
- }
194
- this.context = null
195
- Task.isRunning = false
196
- Task.stackDown()
197
- return shouldRun
198
- }
199
-
200
- get isComplete () {
201
- return this._isComplete ?? false
202
- }
203
-
204
- set isComplete (value) {
205
- this._isComplete = value
206
- }
207
-
208
- static get isRunning () {
209
- return this._isRunning ?? false
210
- }
211
-
212
- static set isRunning (value) {
213
- this._isRunning = value
214
- }
215
-
216
- static get mapCacheToSource () {
217
- return (this._mapCacheToSource = this._mapCacheToSource || {})
218
- }
219
-
220
- /** @returns {[Task]} */
221
- static get described () {
222
- return (this._described = this._described || [])
223
- }
224
-
225
- /** @returns {[Task]} */
226
- static get items () {
227
- return (this._items = this._items || [])
228
- }
229
-
230
- /** @returns {[Task]} */
231
- static get stack () {
232
- return (this._stack = this._stack || [])
233
- }
234
-
235
- /** @returns {Task} */
236
- static get current () {
237
- return this.stack[this.stack.length - 1]
238
- }
239
-
240
- static get currentFile () {
241
- return this._currentFile
242
- }
243
-
244
- static set currentFile (filePath) {
245
- this._currentFile = filePath
246
- }
247
-
248
- static get clonedItems () {
249
- return this.items.map(task => task.clone())
250
- }
251
-
252
- /**
253
- * @param {Task} task
254
- */
255
- static stackUp (task) {
256
- this.stack.push(task)
257
- }
258
-
259
- static stackDown () {
260
- this.stack.pop()
261
- }
262
-
263
- static async load ({
264
- cwd = process.cwd(),
265
- scripts,
266
- cachePath = path.join(cwd, 'migrations/cache'),
267
- logger
268
- }) {
269
- logger.info(`Task -- using cache path ${cachePath}`)
270
- if (!fs.existsSync(cachePath)) fs.mkdirSync(cachePath)
271
- const toDelete = await new Promise(resolve => {
272
- globs([
273
- '*.js'
274
- ], { cwd: cachePath, absolute: true }, (err, files) => resolve(err ? null : files))
275
- })
276
- toDelete.forEach(filePath => fs.rmSync(filePath))
277
- let i = 0
278
- for (const filePath of scripts) {
279
- const cachedPath = path.join(cachePath, `a${++i}.js`).replace(/\\/g, '/')
280
- Task.mapCacheToSource[cachedPath] = filePath.replace(/\\/g, '/')
281
- fs.copyFileSync(filePath, cachedPath)
282
- }
283
- fs.writeJsonSync(path.join(cachePath, 'package.json'), {
284
- name: 'migrations',
285
- type: 'module'
286
- })
287
- await new Promise(resolve => exec('npm install', { cwd: cachePath }, resolve))
288
- const modules = await new Promise(resolve => {
289
- globs([
290
- '*.js'
291
- ], { cwd: cachePath, absolute: true }, (err, files) => resolve(err ? null : files))
292
- })
293
- for (const filePath of modules) {
294
- Task.currentFile = filePath
295
- try {
296
- await import('file://' + filePath.replace(/\\/g, '/'))
297
- for (const task of Task.described) {
298
- Task.items.push(task)
299
- Task.stackUp(task)
300
- await task.load()
301
- Task.stackDown()
302
- }
303
- } catch (error) {
304
- logger.error(`Task -- ${error}`)
305
- error.stack = Object.entries(Task.mapCacheToSource).reduce((stack, [cache, source]) => {
306
- return stack.replaceAll(cache, source)
307
- }, error.stack)
308
- throw error
309
- }
310
- Task.described.length = 0
311
- }
312
- }
313
-
314
- static async runApplicable ({ cwd = process.cwd(), journal, logger }) {
315
- // TODO: don't output task description for search
316
- const clonedTasks = Task.clonedItems
317
- while (true) {
318
- const toRun = []
319
- logger.debug('Task -- Checking for applicable tasks')
320
- for (const task of clonedTasks.filter(item => !item.isComplete)) {
321
- const isApplicable = await task.isApplicable({ cwd, journal, logger })
322
- if (isApplicable) toRun.push(task)
323
- }
324
- logger.debug(`Task -- ${toRun.length} tasks applicable`)
325
- const isExhausted = (!toRun.length)
326
- if (isExhausted) break
327
- logger.debug('Task -- Running applicable tasks')
328
- // TODO: output task description only on run
329
- const lastJournalEntryIndex = journal.lastEntryIndex
330
- for (const task of toRun) {
331
- const { hasErrored } = await task.run({ cwd, journal, logger })
332
- if (hasErrored) {
333
- journal.undoToIndex(lastJournalEntryIndex)
334
- break
335
- }
336
- }
337
- logger.debug('Task -- Applicable tasks finished')
338
- const hasChanged = (lastJournalEntryIndex !== journal.lastEntryIndex)
339
- if (!hasChanged) break
340
- }
341
- prettyPrintJournal(journal, logger)
342
- }
343
-
344
- static async runTests ({ cwd = process.cwd(), logger }) {
345
- for (const task of Task.clonedItems) {
346
- logger.info(`Task -- Testing: ${task.description}`)
347
- for (const test of task.tests) {
348
- const {
349
- // description,
350
- shouldRun,
351
- shouldStop,
352
- shouldError,
353
- fromPlugins,
354
- originalFromPlugins,
355
- toPlugins,
356
- content
357
- } = test()
358
- const journal = new Journal({
359
- logger,
360
- data: {
361
- content,
362
- fromPlugins,
363
- originalFromPlugins,
364
- toPlugins
365
- }
366
- })
367
- const {
368
- hasErrored,
369
- hasStopped,
370
- hasRun
371
- } = await task.run({ cwd, journal, logger })
372
- const isPassed = (shouldError && hasErrored) ||
373
- (shouldRun && hasRun) ||
374
- (shouldRun === false && hasRun === false) ||
375
- (shouldStop && hasStopped && !hasErrored)
376
- logger.info(`> ${isPassed ? 'Passed' : 'Failed'}`)
377
- prettyPrintJournal(journal, logger)
378
- }
379
- }
380
- }
381
- }
1
+ import globs from 'globs'
2
+ import path from 'path'
3
+ import fs from 'fs-extra'
4
+ import chalk from 'chalk'
5
+ import TaskContext from './TaskContext.js'
6
+ import Journal from '../lib/Journal.js'
7
+ import { exec } from 'child_process'
8
+
9
+ function makeTable(items, columnNames) {
10
+ // calculate the max column widths for the items
11
+ const colWidths = items.reduce((colWidths, item) => {
12
+ Object.entries(item).forEach(([colName, value]) => {
13
+ colWidths[colName] = Math.max(colWidths[colName] ?? 0, value !== undefined ? String(value).length : 0)
14
+ if (!colWidths[colName]) return
15
+ // Include column header if data has value
16
+ colWidths[colName] = Math.max(colWidths[colName], colName.length)
17
+ })
18
+ return colWidths
19
+ }, {})
20
+ // produce a well spaced column names header
21
+ const header = columnNames.map(colName => {
22
+ if (!colWidths[colName]) return ''
23
+ return chalk.cyan(String(colName).padEnd(colWidths[colName] + 1, ' '))
24
+ }).join('')
25
+ // produce a well spaced data table
26
+ const body = items.map(item => {
27
+ return columnNames.map(colName => {
28
+ if (!colWidths[colName]) return ''
29
+ return String(item[colName]).padEnd(colWidths[colName] + 1, ' ')
30
+ }).join('')
31
+ }).join('\n')
32
+ return `${header}\n${body}`
33
+ }
34
+
35
+ function prettyPrintJournal(journal, logger) {
36
+ const lines = []
37
+ // collect all of the appropriate information about the change
38
+ for (const entry of journal.entries) {
39
+ const type = (entry.keys[0] === 'fromPlugins')
40
+ ? 'plugin'
41
+ : entry._type
42
+ const name = entry._name
43
+ const id = entry._id
44
+ const action = entry.method
45
+ const property = entry.keys.slice(2).join('.')
46
+ const value = JSON.stringify(entry.value)
47
+ const prev = JSON.stringify(entry.previous)
48
+ lines.push({ type, name, id, '#': lines.length, action, property, value, prev })
49
+ }
50
+ let lineIndex = 0
51
+ const output = []
52
+ while (lineIndex < lines.length) {
53
+ // collect lines with identical subject name and id values
54
+ const fromLineIndex = lineIndex
55
+ let toLineIndex = lineIndex
56
+ const line = lines[lineIndex]
57
+ for (let i = lineIndex; i < lines.length; i++) {
58
+ const nextLine = lines[i]
59
+ if (nextLine.name !== line.name || nextLine.id !== line.id || nextLine.type !== line.type) break
60
+ toLineIndex = i
61
+ }
62
+ // print a grouped summary of the above lines with a single common subject line
63
+ const printLines = lines.slice(fromLineIndex, toLineIndex + 1)
64
+ const subjectTable = makeTable([printLines[0]], ['type', 'name', 'id'])
65
+ const propertyChangeTable = makeTable(printLines, ['#', 'action','property','value'])
66
+ output.push(`${subjectTable}\n${propertyChangeTable}\n`)
67
+ lineIndex = toLineIndex + 1
68
+ }
69
+ if (!output.length) return logger.info(`No changes where made`)
70
+ logger.info(`Summary of changes:\n\n${output.join('\n')}`)
71
+ }
72
+
73
+ export default class Task {
74
+ constructor ({
75
+ description = '',
76
+ tests = [],
77
+ steps = [],
78
+ filePath = Task.currentFile,
79
+ load = () => {}
80
+ } = {}) {
81
+ this.description = description
82
+ this.tests = tests
83
+ this.steps = steps
84
+ this.load = load
85
+ this.filePath = filePath
86
+ if (!load) return // Assumed cloned
87
+ Task.described.push(this)
88
+ }
89
+
90
+ clone () {
91
+ return new Task({
92
+ description: this.description,
93
+ tests: this.tests,
94
+ steps: this.steps,
95
+ filePath: this.filePath,
96
+ load: null
97
+ })
98
+ }
99
+
100
+ getImmediateNextStepsOfType (stepIndex = 0, type) {
101
+ const nextFilters = []
102
+ const nextSteps = this.steps.slice(stepIndex)
103
+ for (const step of nextSteps) {
104
+ if (step.type === type) {
105
+ nextFilters.push(step)
106
+ continue
107
+ }
108
+ break
109
+ }
110
+ return nextFilters
111
+ }
112
+
113
+ getImmediateNextIndexOfType (stepIndex = 0, type) {
114
+ const nextSteps = this.steps.slice(stepIndex)
115
+ for (const nextStepIndex in nextSteps) {
116
+ const step = nextSteps[nextStepIndex]
117
+ if (step.type === type) {
118
+ return parseInt(nextStepIndex) + stepIndex
119
+ }
120
+ }
121
+ return -1
122
+ }
123
+
124
+ async run ({ journal, logger }) {
125
+ logger.debug(`Task -- Running ${this.description}`)
126
+ let shouldContinue = false
127
+ const lastJournalEntryIndex = journal.lastEntryIndex
128
+ this.context = new TaskContext({
129
+ fromPlugins: journal.subject.fromPlugins,
130
+ originalFromPlugins: journal.subject.originalFromPlugins,
131
+ toPlugins: journal.subject.toPlugins,
132
+ content: journal.subject.content,
133
+ journal
134
+ })
135
+ Task.isRunning = true
136
+ Task.stackUp(this)
137
+ const wheres = this.getImmediateNextStepsOfType(0, 'where')
138
+ let stepIndex = 0
139
+ const stepCount = this.steps.length
140
+ while (stepIndex < stepCount) {
141
+ if (stepIndex >= wheres.length) {
142
+ this.context.hasRun = true
143
+ }
144
+ const step = this.steps[stepIndex]
145
+ try {
146
+ shouldContinue = await step(this.context)
147
+ } catch (err) {
148
+ logger.error(`Task -- shouldContinue errored ${err}`)
149
+ this.context.errors.push(err)
150
+ this.context.hasErrored = true
151
+ // Undo changes from this errored migrations
152
+ journal.undoToIndex(lastJournalEntryIndex)
153
+ stepIndex = this.getImmediateNextIndexOfType(stepIndex, 'error')
154
+ shouldContinue = (stepIndex !== -1)
155
+ if (shouldContinue) continue
156
+ }
157
+ if (shouldContinue) {
158
+ stepIndex++
159
+ continue
160
+ }
161
+ this.context.hasStopped = true
162
+ break
163
+ }
164
+ this.isComplete = true
165
+ // TODO: update current plugin version
166
+
167
+ const result = this.context
168
+ this.context = null
169
+ Task.stackDown()
170
+ Task.isRunning = false
171
+ return result
172
+ };
173
+
174
+ async isApplicable ({ journal }) {
175
+ Task.isRunning = true
176
+ Task.stackUp(this)
177
+ this.context = new TaskContext({
178
+ fromPlugins: journal.subject.fromPlugins,
179
+ originalFromPlugins: journal.subject.originalFromPlugins,
180
+ toPlugins: journal.subject.toPlugins,
181
+ content: journal.subject.content,
182
+ journal
183
+ })
184
+ let shouldRun = true
185
+ const wheres = this.getImmediateNextStepsOfType(0, 'where')
186
+
187
+ for (const where of wheres) {
188
+ this.isRunning = true
189
+ const result = await where(this.context)
190
+ this.isRunning = false
191
+ if (result) continue
192
+ shouldRun = false
193
+ break
194
+ }
195
+ this.context = null
196
+ Task.isRunning = false
197
+ Task.stackDown()
198
+ return shouldRun
199
+ }
200
+
201
+ get isComplete () {
202
+ return this._isComplete ?? false
203
+ }
204
+
205
+ set isComplete (value) {
206
+ this._isComplete = value
207
+ }
208
+
209
+ static get isRunning () {
210
+ return this._isRunning ?? false
211
+ }
212
+
213
+ static set isRunning (value) {
214
+ this._isRunning = value
215
+ }
216
+
217
+ static get mapCacheToSource () {
218
+ return (this._mapCacheToSource = this._mapCacheToSource || {})
219
+ }
220
+
221
+ /** @returns {[Task]} */
222
+ static get described () {
223
+ return (this._described = this._described || [])
224
+ }
225
+
226
+ /** @returns {[Task]} */
227
+ static get items () {
228
+ return (this._items = this._items || [])
229
+ }
230
+
231
+ /** @returns {[Task]} */
232
+ static get stack () {
233
+ return (this._stack = this._stack || [])
234
+ }
235
+
236
+ /** @returns {Task} */
237
+ static get current () {
238
+ return this.stack[this.stack.length - 1]
239
+ }
240
+
241
+ static get currentFile () {
242
+ return this._currentFile
243
+ }
244
+
245
+ static set currentFile (filePath) {
246
+ this._currentFile = filePath
247
+ }
248
+
249
+ static get clonedItems () {
250
+ return this.items.map(task => task.clone())
251
+ }
252
+
253
+ /**
254
+ * @param {Task} task
255
+ */
256
+ static stackUp (task) {
257
+ this.stack.push(task)
258
+ }
259
+
260
+ static stackDown () {
261
+ this.stack.pop()
262
+ }
263
+
264
+ static async load ({
265
+ cwd = process.cwd(),
266
+ scripts,
267
+ cachePath = path.join(cwd, 'migrations/cache'),
268
+ logger
269
+ }) {
270
+ logger.info(`Task -- using cache path ${cachePath}`)
271
+ if (!fs.existsSync(cachePath)) fs.mkdirSync(cachePath)
272
+ const toDelete = await new Promise(resolve => {
273
+ globs([
274
+ '*.js'
275
+ ], { cwd: cachePath, absolute: true }, (err, files) => resolve(err ? null : files))
276
+ })
277
+ toDelete.forEach(filePath => fs.rmSync(filePath))
278
+ let i = 0
279
+ for (const filePath of scripts) {
280
+ const cachedPath = path.join(cachePath, `a${++i}.js`).replace(/\\/g, '/')
281
+ Task.mapCacheToSource[cachedPath] = filePath.replace(/\\/g, '/')
282
+ fs.copyFileSync(filePath, cachedPath)
283
+ }
284
+ fs.writeJsonSync(path.join(cachePath, 'package.json'), {
285
+ name: 'migrations',
286
+ type: 'module'
287
+ })
288
+ await new Promise(resolve => exec('npm install', { cwd: cachePath }, resolve))
289
+ const modules = await new Promise(resolve => {
290
+ globs([
291
+ '*.js'
292
+ ], { cwd: cachePath, absolute: true }, (err, files) => resolve(err ? null : files))
293
+ })
294
+ for (const filePath of modules) {
295
+ Task.currentFile = filePath
296
+ try {
297
+ await import('file://' + filePath.replace(/\\/g, '/'))
298
+ for (const task of Task.described) {
299
+ Task.items.push(task)
300
+ Task.stackUp(task)
301
+ await task.load()
302
+ Task.stackDown()
303
+ }
304
+ } catch (error) {
305
+ logger.error(`Task -- ${error}`)
306
+ error.stack = Object.entries(Task.mapCacheToSource).reduce((stack, [cache, source]) => {
307
+ return stack.replaceAll(cache, source)
308
+ }, error.stack)
309
+ throw error
310
+ }
311
+ Task.described.length = 0
312
+ }
313
+ }
314
+
315
+ static async runApplicable ({ cwd = process.cwd(), journal, logger }) {
316
+ // TODO: don't output task description for search
317
+ const clonedTasks = Task.clonedItems
318
+ while (true) {
319
+ const toRun = []
320
+ logger.debug('Task -- Checking for applicable tasks')
321
+ for (const task of clonedTasks.filter(item => !item.isComplete)) {
322
+ const isApplicable = await task.isApplicable({ cwd, journal, logger })
323
+ if (isApplicable) toRun.push(task)
324
+ }
325
+ logger.debug(`Task -- ${toRun.length} tasks applicable`)
326
+ const isExhausted = (!toRun.length)
327
+ if (isExhausted) break
328
+ logger.debug('Task -- Running applicable tasks')
329
+ // TODO: output task description only on run
330
+ const lastJournalEntryIndex = journal.lastEntryIndex
331
+ for (const task of toRun) {
332
+ const { hasErrored } = await task.run({ cwd, journal, logger })
333
+ if (hasErrored) {
334
+ journal.undoToIndex(lastJournalEntryIndex)
335
+ break
336
+ }
337
+ }
338
+ logger.debug('Task -- Applicable tasks finished')
339
+ const hasChanged = (lastJournalEntryIndex !== journal.lastEntryIndex)
340
+ if (!hasChanged) break
341
+ }
342
+ prettyPrintJournal(journal, logger)
343
+ }
344
+
345
+ static async runTests ({ cwd = process.cwd(), logger }) {
346
+ for (const task of Task.clonedItems) {
347
+ logger.info(`Testing -- ${task.description}`)
348
+ for (const test of task.tests) {
349
+ const {
350
+ // description,
351
+ shouldRun,
352
+ shouldStop,
353
+ shouldError,
354
+ fromPlugins = [],
355
+ originalFromPlugins = [],
356
+ toPlugins = [],
357
+ content = []
358
+ } = test()
359
+ const journal = new Journal({
360
+ logger,
361
+ data: {
362
+ content,
363
+ fromPlugins,
364
+ originalFromPlugins,
365
+ toPlugins
366
+ }
367
+ })
368
+ const {
369
+ hasErrored,
370
+ hasStopped,
371
+ hasRun
372
+ } = await task.run({ cwd, journal, logger })
373
+ const isPassed = (shouldError && hasErrored) ||
374
+ (shouldStop && hasStopped && !hasErrored) ||
375
+ (shouldRun && hasRun && !hasErrored && !hasStopped) ||
376
+ (shouldRun === false && hasRun === false)
377
+ if (!isPassed) {
378
+ logger.info(chalk.red('> Failed'))
379
+ } else {
380
+ logger.info(chalk.greenBright('> Passed'))
381
+ }
382
+ prettyPrintJournal(journal, logger)
383
+ }
384
+ }
385
+ }
386
+ }