diff --git a/nextgen/task-builder/src/tasks_builder.rs b/nextgen/task-builder/src/tasks_builder.rs index e101d2497c4..64a3da0011b 100644 --- a/nextgen/task-builder/src/tasks_builder.rs +++ b/nextgen/task-builder/src/tasks_builder.rs @@ -16,6 +16,21 @@ use std::hash::Hash; use std::path::Path; use tracing::trace; +// This is a standalone function as recursive closures are not possible! +fn extract_config<'builder, 'proj>( + task_id: &'builder Id, + tasks_map: &'builder FxHashMap<&'proj Id, &'proj TaskConfig>, + configs: &'builder mut Vec<&'proj TaskConfig>, +) { + if let Some(config) = tasks_map.get(task_id) { + if let Some(extend_task_id) = &config.extends { + extract_config(extend_task_id, tasks_map, configs); + } + + configs.push(*config); + } +} + #[derive(Debug)] pub struct DetectPlatformEvent { pub enabled_platforms: Vec, @@ -529,25 +544,8 @@ impl<'proj> TasksBuilder<'proj> { fn get_config_inherit_chain(&self, id: &Id) -> Vec<&TaskConfig> { let mut configs = vec![]; - if let Some(config) = self.global_tasks.get(id) { - if let Some(extends_from) = &config.extends { - if let Some(extends_config) = self.global_tasks.get(extends_from) { - configs.push(*extends_config); - } - } - - configs.push(*config); - } - - if let Some(config) = self.local_tasks.get(id) { - if let Some(extends_from) = &config.extends { - if let Some(extends_config) = self.local_tasks.get(extends_from) { - configs.push(*extends_config); - } - } - - configs.push(*config); - } + extract_config(id, &self.global_tasks, &mut configs); + extract_config(id, &self.local_tasks, &mut configs); configs } diff --git a/nextgen/task-builder/tests/__fixtures__/builder/extends/moon.yml b/nextgen/task-builder/tests/__fixtures__/builder/extends/moon.yml index c7501e6eaa0..2ba37affad0 100644 --- a/nextgen/task-builder/tests/__fixtures__/builder/extends/moon.yml +++ b/nextgen/task-builder/tests/__fixtures__/builder/extends/moon.yml @@ -13,6 +13,12 @@ tasks: options: mergeArgs: prepend + extend-args-again: + extends: extend-args + args: --bail + options: + mergeArgs: append + extend-inputs: extends: base inputs: diff --git a/nextgen/task-builder/tests/tasks_builder_test.rs b/nextgen/task-builder/tests/tasks_builder_test.rs index 5e579930244..130124943a1 100644 --- a/nextgen/task-builder/tests/tasks_builder_test.rs +++ b/nextgen/task-builder/tests/tasks_builder_test.rs @@ -1197,5 +1197,15 @@ mod tasks_builder { assert!(task.options.persistent); assert_eq!(task.options.retry_count, 3); } + + #[tokio::test] + async fn can_create_extends_chains() { + let sandbox = create_sandbox("builder"); + let tasks = build_tasks(sandbox.path(), "extends/moon.yml").await; + let task = tasks.get("extend-args-again").unwrap(); + + assert_eq!(task.command, "lint"); + assert_eq!(task.args, vec!["./src", "--fix", "--bail"]); + } } } diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 977f0ddd8f8..e3cde90fd63 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +#### 🚀 Updates + +- Added an `extends` field to task configurations. This allows tasks to extend and inherit settings + from sibling tasks. + ## 1.11.1 #### 🐞 Fixes diff --git a/packages/types/src/tasks-config.ts b/packages/types/src/tasks-config.ts index 47c228c370c..917c8e3bdfc 100644 --- a/packages/types/src/tasks-config.ts +++ b/packages/types/src/tasks-config.ts @@ -39,6 +39,7 @@ export interface PartialTaskConfig { command?: PartialTaskCommandArgs | null; deps?: string[] | null; env?: Record | null; + extends?: string | null; inputs?: string[] | null; local?: boolean | null; options?: PartialTaskOptionsConfig | null; @@ -82,6 +83,7 @@ export interface TaskConfig { command: TaskCommandArgs; deps: string[]; env: Record; + extends: string | null; inputs: string[] | null; local: boolean | null; options: TaskOptionsConfig; diff --git a/website/blog/2023-08-21_moon-v1.12.mdx b/website/blog/2023-08-21_moon-v1.12.mdx new file mode 100644 index 00000000000..6bff0dd54e2 --- /dev/null +++ b/website/blog/2023-08-21_moon-v1.12.mdx @@ -0,0 +1,92 @@ +--- +slug: moon-v1.12 +title: moon v1.12 - Task extending +authors: [milesj] +tags: [tasks, inheritance] +# image: ./img/moon/v1.11.png +--- + +??? + + + +## Extending sibling or inherited tasks + +Three months ago, we posted an +[RFC on how to support task extending / task variants](https://github.com/moonrepo/moon/issues/849). +On paper this doesn't sound like a hard problem to solve, but internally it would of been an uphill +battle to implement. Thanks to previous releases from the past few months, and the rewrite of the +project graph, task builder, and more, this implementation was a breeze. To finalize the RFC, we +went with option 2, by adding a new `extends` field to task configurations. + +With this new addition, we can now rewrite this old configuration, which was needlessly +repetitive... + +```yaml title="moon.yml" +tasks: + lint: + command: 'eslint .' + inputs: + - '@globs(sources)' + - '@globs(tests)' + - '*.js' + - '.eslintrc.js' + - 'tsconfig.json' + - '/.eslintignore' + - '/.eslintrc.js' + - '/tsconfig.eslint.json' + - '/tsconfig.options.json' + + lint-fix: + command: 'eslint . --fix' + local: true + inputs: + - '@globs(sources)' + - '@globs(tests)' + - '*.js' + - '.eslintrc.js' + - 'tsconfig.json' + - '/.eslintignore' + - '/.eslintrc.js' + - '/tsconfig.eslint.json' + - '/tsconfig.options.json' +``` + +Into the following configuration. + +```yaml title="moon.yml" +tasks: + lint: + command: 'eslint .' + inputs: + - '@globs(sources)' + - '@globs(tests)' + - '*.js' + - '.eslintrc.js' + - 'tsconfig.json' + - '/.eslintignore' + - '/.eslintrc.js' + - '/tsconfig.eslint.json' + - '/tsconfig.options.json' + + lint-fix: + extends: 'lint' + args: '--fix' + local: true +``` + +We're very happy with this solution, as it's far more readable, maintainable, and doesn't introduce +yet another paradigm to learn. Our goal was to be as familiar as possible, while providing extensive +functionality behind the scenes, which we believe to have achieved. + +Some other interesting facts around task extending: + +- When extending a task, [merge strategies](/docs/concepts/task#merge-strategies) are applied in a + similar fashion to inheritance. +- Inherited tasks can be extended from by project-level tasks. +- It's possible to create multiple extended chains. + +## Other changes + +View the [official release](https://github.com/moonrepo/moon/releases/tag/v1.12.0) for a full list +of changes. diff --git a/website/docs/config/project.mdx b/website/docs/config/project.mdx index 529c6215b44..5e7473c3d22 100644 --- a/website/docs/config/project.mdx +++ b/website/docs/config/project.mdx @@ -350,6 +350,30 @@ tasks: command: 'tsc' ``` +### `extends` + + + +The `extends` field can be used to extend the settings from a sibling task within the same project, +or [inherited from the global scope](../concepts/task#inheritance). This is useful for composing +similar tasks with different arguments or options. + +When extending another task, the same [merge strategies](../concepts/task#merge-strategies) used for +inheritance are applied. + +```yaml title="moon.yml" +tasks: + lint: + command: 'eslint .' + inputs: + - 'src/**/*' + + lint-fix: + extends: 'lint' + args: '--fix' + local: true +``` + ### `command` diff --git a/website/static/schemas/project.json b/website/static/schemas/project.json index 31ee32ff42d..5b36320d4b4 100644 --- a/website/static/schemas/project.json +++ b/website/static/schemas/project.json @@ -641,6 +641,16 @@ } ] }, + "extends": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "inputs": { "anyOf": [ { diff --git a/website/static/schemas/tasks.json b/website/static/schemas/tasks.json index b29c11efd2a..0ad6d4e6f6e 100644 --- a/website/static/schemas/tasks.json +++ b/website/static/schemas/tasks.json @@ -159,6 +159,16 @@ } ] }, + "extends": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "inputs": { "anyOf": [ {