cloe
v0.1.3
Published
framework for building developer tools on the cli and in editors
Downloads
7
Readme
A framework for building developer tools on the cli and in editors.
It's not quite ready for use yet, and will likely change a lot between now and then, but maybe this will get you excited!
Install
I recommend installing cloe in each project where you have some custom code for it, and a global install can be used for running cloe in arbitrary places.
npm install --save-dev cloe
npm install --global cloe
# or
yarn add --dev cloe
yarn global add cloe
CLI
The basic interface to cloe is on the command line. We'll be using some simple commands, but there's much more to cloe, so continue reading! First, let's create a file.
$ echo "Hello, world!" > test.txt
Then we'll invoke a "cloe behavior" on the file.
$ cloe -f test.txt readFile
Hello, world!
Here, the -f
flag tells cloe which file(s) to execute the command on. Then we have a
positional argument for the command to run, in this case, it's "readFile".
Some commands take arguments. To specify them, we use square brackets. Make sure to leave a space around the brackets.
$ cloe [ cloe.echo "Anybody there?" ]
Anybody there?
Concepts
At the core of cloe you'll find "behaviors". Many are defined for you, and you can define your own. Most importantly, you can override behaviors per-project.
Let's look at a simplified version of readFile
, which we used earlier. Built-in
behaviors are prefixed with 'cloe.', but you can define your own with any name you like.
The file needs to be named readFile.js
, and reside in a directory cloe knows to look in.
In this case, the file can be found in src/behaviors/readFile.js
.
import * as fs from 'fs';
import promisify from 'util.promisify';
const readFile = promisify(fs.readFile);
export default async function cloeReadFile(ctx) {
// Get the file we're currently running on
const filePath = ctx.filePath;
// Attempt to read the file
try {
content = await readFile(filePath, encoding);
} catch (e) {
// Handling errors is good, as we can communicate more clearly what went wrong.
if (e.code === 'EISDIR') {
throw ctx.error(`Tried to read a directory`, { reason: 'is_directory' });
}
throw e;
}
return content;
}
// Reads the file, prints it to screen
export async function main(ctx) {
const content = await ctx.exec(`readFile`);
console.log(content);
}
There are two functions here. Think of them like a library and and a program respectively.
The cloeReadFile
function can be run by other behaviors, allowing you to compose them to
accomplish any task. The main
function is invoked when we, the user, directly invoke the
behavior readFile
, e.g. via the CLI.
There's quite a lot going on in this code, but we're still covering the core concepts! More on that and this 'ctx' thing later.
Files
Many default implementations of behaviors exist in the cloe repo, but these implementations might not fit your requirements or project. Because behaviors are kept small, relying on other behaviors when possible, and avoiding duplication, you can pick specific things to override in your project. You can also define new behaviors in your project, or across all projects on your computer.
Let's take a look at another behavior called "resolveImport". The implementation doesn't
matter, but the usage string in the source says "Usage: resolveImport './foo.js'". We can
attempt to run it from the CLI. For this example, assume we have src/app/foo.js
and it
contains import illusive from '~src/utils/illusive';
. Let's run a behavior that should
(cross your fingers) give us the absoltue path to this illusive.js
file.
$ cloe -f src/app/foo.js [ resolveImport '~src/utils/illusive.js' ]
resolveImport: Failed to resolve module
- resolveImport(
"~src/utils/illusive.js",
)
Hey, wait a minute! What is this '~' thing? Well, cloe doesn't know either. The default
implementation of resolveImport
uses the node.js resolve algorithm, but it seems we're
using a custom way of resolving modules; perhaps a babel plugin or webpack loader (yes, I
actually do this in some projects, it's really great).
We'd still like to use the rest of cloe, we don't want to fork the whole thing, and we want everything that tries to resolve a JS import to use our custom implementation - cloe core files, our own files, maybe some things we got from npm, etc.
Easy enough; we'll copy over the file from cloe, and make the changes. To do this, we'll create a new directory in our project to keep our cloe files.
mkdir -p .cloe
Then we can take the default implementation, and put it in ./.cloe/resolveImport.js
.
I've added a small fix to handle our custom imports.
import relative from 'require-relative';
/*
Usage: resolveImport './foo.js'
Returns: null
*/
export default async function cloeJsResolveImport(ctx, importPath) {
let filePath = ctx.filePath;
if (!filePath) throw ctx.error(`Must be run on a file`, { reason: 'no_ctx_file' });
// THIS IS OUR CHANGE
if (filePath[0] === '~') {
filePath = ctx.cwd + '/' + filePath.slice(1);
}
// END OF CHANGE
const fromFile = filePath.replace(/\/[^\/]+$/, '');
try {
return relative.resolve(importPath, fromFile);
} catch (e) {
if (/Cannot find module/.test(e.message)) {
throw ctx.error(`Failed to resolve module`, { reason: 'not_found' });
}
throw e;
}
}
Now any time resolveImport
is invoked in this project - directly or indirectly - it'll
use our custom implementation.
Let's give it a whirl.
$ cloe -f src/app/foo.js [ resolveImport '~src/utils/illusive.js' ]
/home/me/projects/example/src/utils/illusive.js
Success! This is all the documentation there is so far. Feel free to dive into the source, and file a github issue if you need help. All feedback is very much appreciated.