Skip to content

Instantly share code, notes, and snippets.

@timhall
Created July 27, 2017 13:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timhall/6f77d870c7dabb8c2d9c5a44d1da41d1 to your computer and use it in GitHub Desktop.
Save timhall/6f77d870c7dabb8c2d9c5a44d1da41d1 to your computer and use it in GitHub Desktop.
Task / Plugin Unification
Error in user YAML: (<unknown>): mapping values are not allowed in this context at line 1 column 11
---
title: RFC: Task / Plugin Unification
---

Summary

This RFC presents an alternative plugin API that matches the current task API.

Unifying tasks and plugins should encourage task reuse (as plugins), simplify the overall API, and allow a single core mental model for how taskr tasks and plugins function.

Detailed Design

Generally, a unified task or plugin has the following shape:

type Result = void | any | Task;

function (task: Task, ...options: any[]): Result | Promise<Result> {
  // Transform given task with options
}
  • All tasks and plugins are passed to Bluebird.coroutine, so all function types, including async functions and generators, are supported.
  • If a Task is returned, taskr will continue work from that task's state, allowing task chains to be created (more details below).
  • For non-task returns, the current val approach is used, to pass values to subsequent tasks.

To utilize this new approach in a semver-minor-friendly manner, plugins using the unified model are denoted with named exports and use peerDependencies to specify a minimum supported taskr version. This should allow changes while maintaining compatibility with the existing ecosystem.

// Existing approach
module.exports = function(task) {
  task.plugin('name', { files: true }, function * (files) {
    // ...
  });
};

// Unified approach
exports.name = function * (task) {
  const files = task._.files;
  // ...
};

It's fairly straightforward to match existing functionality with wrapper utilities:

// Before
module.exports = function(task) {
  task.plugin('sass', { every: true }, function * (file) {
    // ...
  });
}

// After
const every = require('@taskr/utils').every;

exports.sass = function(task) {
  return every(task, function * (file) {
    // ...
  });
};

To accomodate this new approach, an updated form of task.run is introduced which runs the task/plugin directly.

async function build(task) {
  task.a().b() === task.run(a).run(task => b(task));
  task.start('js') === task.run(js);
  task.parallel(['js', 'css']) === task.parallel([js, css]);
}

Finally, to allow chaining, tasks continue from a previous task if a Task is returned.

async function build(task) {
  return task.run(js).target('build');
}

async function js(task) {
  // By returning the resulting task from source -> babel
  // the parent can continue chain
  return task.source('js/**/*').babel();
}

In addition to allowing for chaining, this removes unexpected side effects from tasks run in succession, see #289, by returning a new task from each step rather than mutating the initial task.

async function build(task) {
  await task.a().b().c();
  //         ^   ^   ^
  // a new, lightweight task is generated at each step
  // -> no side-effects on original task

  // task has not been mutated, can safely run subsequent tasks
  await task.d().e().f();
}

Future Possibilities

This new approach allows for many interesting future possibilities, e.g. merging tasks:

async function build(task) {
  return task.merge(js, css).target('build');
}

async function js(task) {
  return task.source('src/js/**/*.js').typescript().babel();
}
async function css(task) {
  return task.source('src/css/**/*.scss').sass();
}

Conditional tasks:

const production = process.env.NODE_ENV === 'production';

function js(task) {
  task.source('src/js/**/*.js')
    .typescript()
    .babel()
    .check(production, task => task.uglify());
}

Drawbacks

While the side-effect free approach of chaining tasks may be preferred for its "correctness", it may cause compatibility issues with existing systems. Task changes will have to be approached carefully.

Alternatives

These examples are possible with the current version of taskr, but they should be much more straightforward to implement with the unified system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment