Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
883809b
chore: scaffold parallel plugin
apaleslimghost Jul 25, 2024
75dde66
feat: sketch out the parallel task
apaleslimghost Jul 25, 2024
9850745
refactor: rename runTasks to runCommands for clarity
apaleslimghost Jul 29, 2024
758e129
refactor: further split command and task running
apaleslimghost Jul 29, 2024
3850e4e
refactor: add plugin property to task class
apaleslimghost Jul 29, 2024
b0630ba
feat: run specified tasks in parallel
apaleslimghost Jul 29, 2024
c89bcd4
feat: add a Task.stop method and stop parallel tasks on error
apaleslimghost Jul 29, 2024
295a3c4
docs: add readme for parallel plugin
apaleslimghost Jul 29, 2024
d0cfa90
feat: implement stop method for webpack task
apaleslimghost Jun 19, 2025
6025f64
feat(serverless): exit child on stop
apaleslimghost Jun 19, 2025
729d76f
feat(parallel): configurable stopOnError behaviour
apaleslimghost Jun 19, 2025
b6cfedf
feat: implement stop method for Parallel itself
apaleslimghost Jun 19, 2025
58d9ae1
refactor: rename stopOnError to onError and change to a literal union
apaleslimghost Jun 23, 2025
774bfea
test: add basic tests for Parallel task running things in parallel
apaleslimghost Jul 1, 2025
eae0f56
WIP
apaleslimghost Jul 1, 2025
6c6cff0
test: use custom snapshot serialisers for all jest tests
apaleslimghost Jul 1, 2025
5f2514e
test: add tests for parallel error handling
apaleslimghost Jul 1, 2025
2f47c00
feat(node): optionally wait for child process to exit
apaleslimghost Jul 29, 2024
ad17820
feat(serverless): optionally wait for run task to exit
apaleslimghost Jun 19, 2025
bd65c3d
docs: automatically regenerate schema docs
apaleslimghost Jul 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/cli/bin/run
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function main() {
const showHelp = require('../lib/help').default
await showHelp(rootLogger, argv._)
} else {
const { runTasks } = require('../lib')
const { runCommands } = require('../lib')
if (argv['--'].length > 0) {
// The `--` in a command such as `dotcom-tool-kit test:staged --`
// delineates between hooks and file patterns. For example, when the
Expand All @@ -36,9 +36,9 @@ async function main() {
// the command becomes something like `dotcom-tool-kit test:staged --
// index.js`. When this command is executed it runs the configured task
// where the file path arguments would then be extracted.
await runTasks(rootLogger, argv._, argv['--'])
await runCommands(rootLogger, argv._, argv['--'])
} else {
await runTasks(rootLogger, argv._)
await runCommands(rootLogger, argv._)
}
}
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion core/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Logger } from 'winston'
import util from 'util'
import { formatPluginTree } from './messages'

export { runTasks } from './tasks'
export { runCommands } from './tasks'
export { shouldDisableNativeFetch } from './fetch'

export async function listPlugins(logger: Logger): Promise<void> {
Expand Down
99 changes: 57 additions & 42 deletions core/cli/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ import { OptionsForTask } from '@dotcom-tool-kit/plugin'
import type { RootOptions } from '@dotcom-tool-kit/plugin/src/root-schema'
import pluralize from 'pluralize'

type ErrorSummary = {
export type ErrorSummary = {
task: string
error: Error
}

const loadTasks = async (
export async function loadTasks(
logger: Logger,
tasks: OptionsForTask[],
config: ValidConfig
): Promise<Validated<Task[]>> => {
): Promise<Validated<Task[]>> {
const taskResults = await Promise.all(
tasks.map(async ({ task: taskId, options }) => {
tasks.map(async ({ task: taskId, options, plugin }) => {
const entryPoint = config.tasks[taskId]
const taskResult = await importEntryPoint(Task, entryPoint)

Expand All @@ -45,7 +45,8 @@ const loadTasks = async (
logger,
taskId,
config.pluginOptions[entryPoint.plugin.id]?.options ?? {},
parsedOptions.data
parsedOptions.data,
plugin
)
return valid(task)
} else {
Expand All @@ -58,7 +59,54 @@ const loadTasks = async (
return reduceValidated(taskResults)
}

export async function runTasksFromConfig(
export function handleTaskErrors(errors: ErrorSummary[], command: string) {
throw new AggregateError(
errors.map(({ task, error }) => {
error.name = `${styles.task(task)} → ${error.name}`
return error
}),
`${pluralize('error', errors.length, true)} running tasks for ${styles.command(command)}`
)
}

export async function runTasks(
logger: Logger,
config: ValidConfig,
tasks: Task[],
command: string,
files?: string[]
) {
const errors: ErrorSummary[] = []

if (tasks.length === 0) {
logger.warn(`no task configured for ${command}: skipping assignment...`)
}

for (const task of tasks) {
try {
logger.info(styles.taskHeader(`running ${styles.task(task.id)} task`))
await task.run({ files, command, cwd: config.root, config })
} catch (error) {
// if there's an exit code, that's a request from the task to exit early
if (error instanceof ToolKitError && error.exitCode) {
throw error
}

// if not, we allow subsequent hook tasks to run on error
// TODO use validated for this
errors.push({
task: task.id,
error: error as Error
})
}
}

if (errors.length > 0) {
handleTaskErrors(errors, command)
}
}

export async function runCommandsFromConfig(
logger: Logger,
config: ValidConfig,
commands: string[],
Expand Down Expand Up @@ -90,45 +138,12 @@ export async function runTasksFromConfig(
Object.freeze(config)

for (const { command, tasks } of commandTasks) {
const errors: ErrorSummary[] = []

if (tasks.length === 0) {
logger.warn(`no task configured for ${command}: skipping assignment...`)
}

for (const task of tasks) {
try {
logger.info(styles.taskHeader(`running ${styles.task(task.id)} task`))
await task.run({ files, command, cwd: config.root, config })
} catch (error) {
// if there's an exit code, that's a request from the task to exit early
if (error instanceof ToolKitError && error.exitCode) {
throw error
}

// if not, we allow subsequent hook tasks to run on error
// TODO use validated for this
errors.push({
task: task.id,
error: error as Error
})
}
}

if (errors.length > 0) {
throw new AggregateError(
errors.map(({ task, error }) => {
error.name = `${styles.task(task)} → ${error.name}`
return error
}),
`${pluralize('error', errors.length, true)} running tasks for ${styles.command(command)}`
)
}
await runTasks(logger, config, tasks, command, files)
}
}

export async function runTasks(logger: Logger, commands: string[], files?: string[]): Promise<void> {
export async function runCommands(logger: Logger, commands: string[], files?: string[]): Promise<void> {
const config = await loadConfig(logger, { root: process.cwd() })

return runTasksFromConfig(logger, config, commands, files)
return runCommandsFromConfig(logger, config, commands, files)
}
7 changes: 7 additions & 0 deletions jest.config.base.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const path = require('path')

const tsJestConfig = {
tsconfig: 'tsconfig.settings.json',
isolatedModules: true
Expand All @@ -10,6 +12,11 @@ module.exports.config = {
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/*.+(spec|test).[jt]s?(x)'],
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/.+/lib/', '/test/files'],
clearMocks: true,
snapshotSerializers: [
'@relmify/jest-serializer-strip-ansi/always',
path.resolve(__dirname, './jest/serializers/aggregate-error.js'),
path.resolve(__dirname, './jest/serializers/tool-kit-error.js')
],
transform: {
'^.+\\.tsx?$': ['ts-jest', tsJestConfig]
}
Expand Down
15 changes: 15 additions & 0 deletions jest/serializers/aggregate-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
test(value) {
return value instanceof Error && value.name === 'AggregateError'
},

serialize(val, config, indentation, depth, refs, printer) {
return `AggregateError ${printer(val.message, config, indentation, depth, refs)} ${printer(
{ errors: val.errors },
config,
indentation,
depth,
refs
)}`
}
}
15 changes: 15 additions & 0 deletions jest/serializers/tool-kit-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
test(value) {
return value instanceof Error && value.constructor.name === 'ToolKitError'
},

serialize(val, config, indentation, depth, refs, printer) {
return `${printer(val.name, config, indentation, depth, refs)} ${printer(
val.message,
config,
indentation,
depth,
refs
)} ${printer({ details: val.details }, config, indentation, depth, refs)}`
}
}
11 changes: 9 additions & 2 deletions lib/base/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Base } from './base'
import { taskSymbol, typeSymbol } from './symbols'
import type { Logger } from 'winston'
import type { ValidConfig } from '@dotcom-tool-kit/config'
import { Plugin } from '@dotcom-tool-kit/plugin'
import type { Default } from './type-utils'
import type { ReadonlyDeep } from 'type-fest'

Expand Down Expand Up @@ -33,21 +34,27 @@ export abstract class Task<
logger: Logger,
public id: string,
public pluginOptions: z.output<Default<Options['plugin'], z.ZodObject<Record<string, never>>>>,
public options: z.output<Default<Options['task'], z.ZodObject<Record<string, never>>>>
public options: z.output<Default<Options['task'], z.ZodObject<Record<string, never>>>>,
public plugin: Plugin
) {
super()
this.logger = logger.child({ task: id })
}

abstract run(runContext: TaskRunContext): Promise<void>

// not abstract for default behaviour of doing nothing
// eslint-disable-next-line @typescript-eslint/no-empty-function
async stop(): Promise<void> {}
}

export type TaskConstructor = {
new <O extends { plugin: z.ZodTypeAny; task: z.ZodTypeAny }>(
logger: Logger,
id: string,
pluginOptions: Partial<z.infer<O['plugin']>>,
options: Partial<z.infer<O['task']>>
options: Partial<z.infer<O['task']>>,
plugin: Plugin
): Task<O>
}

Expand Down
Loading