Skip to content

Latest commit

 

History

History
266 lines (220 loc) · 8.54 KB

ARCHITECTURE.md

File metadata and controls

266 lines (220 loc) · 8.54 KB

Overview

  • cli parses args and calls the respective command
  • command calls a sequence of tasks
  • tasks do the actual work

The different components of ember-cli

cli()

cli is a small function that gets everything going.

Usage:

var cli = require('cli');

cli({
  cliArgs: argv, // Required
  inputStream: inputStream, // Required
  outputStream: outputStream // Required
})
  .then(...);

UI

In ember-cli we pass an UI instance around. Instead of calling console.log or writing things directly to process.stdout we access those through this wrapper. This makes our code testing friendly because it lets us simulate user input and it lets us verify if the output matches the expected output in tests.

ui.prompt(options).then(...) can be used to get user input. It wraps the inquirer node package. See its documentation for usage instructions. Note: Navigation with arrow keys doesn't work properly on windows, only use prompt types with text input.

ui.write() can be used to write something to the output stream. It's just this.outputStream.write(data); internally.

ui.inputStream, ui.outputStream can be used for things that require a stream. Also nice for testing, e.g. simulating input.

Commands

Located in lib/commands/. They get picked up by requireAsHash() automatically.

The CLI constructs command instances with dependencies including ui, analytics, commands, project, etc. The plan is for these to eventually be constructed and wired up via a dependency injection container.

The following file structure is expected (Demonstrated on the imaginary command develop-ember-cli):

// e.g. commands/develop-ember-cli.js

var Command = require('../models/command');

module.exports = Command.extend({
  name: 'develop-ember-cli',    // Optional, default is the filename
                                // Powered by some magic in getCallerFile()
  key: 'developEmberCLI',       // Optional, default is the camelized name
  description: 'Explanation',   // Optional
  aliases: ['d', 'go-tomster'], // Optional, default is an empty array
  works: 'everywhere',          // 'insideProject', 'outsideProject' or 'everywhere'
                                // Optional, default is 'insideProject'

  availableOptions: [           // Optional, default is an empty array
    {
      name: 'package-name',     // Required
      key: 'packageName'        // Optional, default is the camelized name
      description: '...',       // Optional
      type: String,             // Required
      default: 'ember-cli',     // Optional, default is undefined
      required: false           // Optional, default is false
    },
    ...
  ],
  run: function(options) { // Required
    // options === { packageName, ... }

    // Run tasks and return a promise
  },
  usageInstructions: function() { // Optional
    return 'Usage Instructions...';
  }
});

For more information on possible option types, see the documentation for the nopt library.

Best practice is to use the run() function only to execute tasks. The real work should be done in these tasks, then.

The promise returned by run() should either

  • resolve to undefined
  • reject with an Error instance if the error is unhandled
  • or reject with undefined if it was handled. In this case the command should log something via the ui first.

requireAsHash() assembles from the files in commands/ a hash that looks like this:

{
  DevelopEmberCLI: require('commands/develop-ember-cli'),
  ...
}

Usage instructions formatting

ember serve <arg-option (Default: something)>
  --port (Default: 4200) Description 1
  --important-option (Required) Description 2
  • white: ember serve

  • yellow: <arg-option , >

  • cyan --port, --important-option

  • cyan (Default: something), (Default: 4200)

  • white Description 1, Description 2

  • cyan (Required)

Tasks

Located in lib/tasks. They get picked up by requireAsHash() automatically.

Tasks do the real work. They should also do only one thing: For example there are seperate bower-install and npm-install tasks, not just one unified install task. And they should not call other tasks: For example install-blueprint shouldn't call npm-install directly. That's because the task sequence is determined by the command and thus should be declared there.

The command constructs task instances with dependencies including ui, analytics, project, etc. The plan is for these to eventually be constructed and wired up via a dependency injection container.

A task's run method has to return a promise which resolves or rejects depending on whether it ran through successfully or not.

The promise of a task should either

  • fulfill to undefined
  • reject with an Error instance if the error is unhandled
  • or reject with undefined if it was handled. In this case the task should log something via the ui first.

So, tasks don't have a return value per design.

The file format of a task looks like this:

// tasks/npm-install.js

var Task = require('../task');

module.exports = Task.extend({
  run: function(options) {
    // return promise
  }
});

requireAsHash() assembles from the files in tasks/ a hash that looks like this:

{
  NpmInstall: require('tasks/npm-install'),
  ...
}

Style guide

  • Everything Promise based ( use: lib/ext/promise)
  • Everything async (except require)
  • Short files
  • Tests, tests, tests
  • Recommended line length <= 80 characters
  • No console.log, we've our own logging system
  • HTML and CSS: Double quotes, JavaScript: Single quotes
  • Naming conventions
    • Dasherized (some-thing)
      • file, folder and package names
      • CSS classes
      • HTML tags and attributes
    • Camel case (someThing)
      • JavaScript (and JSON) properties and variables
    • Pascal case (SomeThing)
      • JavaScript class names
    • Acronyms:
      • Okay: url, id, rootURL (property) or URL, URLParser (class)
      • Wrong: Url,rootUrl
      • We stick with how it's done in ember -> rootURL
  • No comma separated var statements (var cool = 123, supercool = 456;)
  • Line break at the end of every file
  • Make constructors take an options object to avoid order-dependence

This list only contains style decisions not already covered by JSHint (e.g. mandatory semicolons and other rules are omitted).

Indentation

Aligned require statments

var RSVP    = require('rsvp');
var Promise = RSVP.Promise;

Multiline return statement

// Correct
return someFunction(
    someArgument,
    otherArgument
  );

// Wrong
return someFunction(
  someArgument,
  otherArgument
);
// Correct
return returnsAPromise()
  .then(...)
  .catch(...);

// Wrong
return retursAPromise().then(...)
.catch(...);

Custom errors

Custom error classes should end with the suffix "Error".

function CustomError() {
  this.stack = (new Error()).stack;
}
CustomError.prototype = Object.create(Error.prototype);
CustomError.prototype.name = 'CustomError';

Also a message property should be set: Either in the constructor or as a property on CustomError.prototype.

Sync vs async

Since JavaScript uses an event loop, the use of blocking and compute intesive operations is discouraged. The general recommendation is to use asynchronous operations.

However, there are exceptions. Node's own require statement is synchronous. It is mainly used at program startup and only for a handful of files. Consequently, although it being synchronous, using it is harmless. Same thing goes for synchronous file globs in combination with require at startup.

The use of asynchronous code is mainly important for file walking operations during runtime. For example for globbing a large amout of files or recursive copying/deleting of folder structures. These operations generally take a long time to complete and would if implemented synchronously disrupt progress animations or delay server responses.

An advantage of asynchronous operations is that libraries can use it to offer increased reliability. For example the popular file deletion library rimraf uses setTimeout and a limited amount of retries after increasing time intervals to mitigate EBUSY errors on windows. Also, libraries can use asynchronicity to offload work onto worker threads. By providing an asynchronous API fixes and optimizations can be implemented transparently without breaking API compatibilty.