Skip to content

General Purpose Build System with JavaScript DSL for Node.js.

License

Notifications You must be signed in to change notification settings

stylemistake/juke-build

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JUKE build

General Purpose Build System with JavaScript DSL for Node.js. Inspired by NUKE.

This project is reaching a mature stage, although a lot of features are still in development. Take a look at our roadmap.

Project goals

Simplicity

Everything should be as simple as possible in all technical aspects. Builds are written in pure JavaScript and provide only the bare minimum for getting the job done. TypeScript is supported, but not required.

Currently it packs the following:

  • A robust dependency model between targets
  • File timestamp checker for inputs/outputs
  • Built-in CLI argument (and environment) parser with a strongly typed Parameter API.
  • Asynchronous execution of external programs via Juke.exec()

You can bring your own tools into the mix, e.g. the glorious google/zx, or native JavaScript tooling, e.g. webpack, with no restrictions imposed by our build framework.

Minimal dependencies

Build system should be native to JavaScript and Node.js, and require nothing but the Node.js executable, i.e. no dependency on npm/yarn or TypeScript compiler.

Strongly typed

Strongly typed API with fully instrospectible build scripts that are written in plain JavaScript, which allows us to parse the build script and generate definition files for tighter integration with other tooling (e.g. CI).

How to build

./build.mjs

General usage

Copy contents of the dist folder anywhere you want to use Juke (and rename it to juke), then create a javascript file for the build script with the following contents (pick one):

ES modules variant (recommended):

// build.mjs
import Juke from './juke/index.js';

Juke.setup({ file: import.meta.url });

// TODO: Declare targets here
export const MyTarget = new Juke.Target({
  // ...
});

ES modules with require:

// build.mjs
import { createRequire } from 'module';

const require = createRequire(import.meta.url);
const Juke = require('./juke');

Juke.setup({ file: import.meta.url });

// TODO: Declare targets here
export const MyTarget = new Juke.Target({
  // ...
});

CommonJS modules variant:

// build.cjs
const Juke = require('./juke');

Juke.setup({ file: __filename });

// TODO: Declare targets here
const MyTarget = new Juke.Target({
  // ...
});

// TODO: Export targets here
module.exports = {
  MyTarget,
};

We recommend using an ES module for the build script, because it allows exporting targets/parameters with a much shorter syntax.

Create targets

Target is a simple container for your build script that defines how it should be executed in relation to other targets. It may have dependencies on other targets, and may have various other conditions for executing the target.

export const Target = new Juke.Target({
  executes: async () => {
    console.log('Hello, world!');
  },
});

Notice: When referencing an unexported target, it must have a name property, which is used in CLI for specifying (and displaying) the target. If you forget to specify a name, it will be displayed as undefined during execution.

const Target = new Juke.Target({
  name: 'foo',
  // ...
});

Normally, name is derived from the name of the exported variable (minus the Target suffix).

Declare dependencies

export const Target = new Juke.Target({
  dependsOn: [OtherTarget],
  // ...
});

Set a default target

When no target is provided via CLI, Juke will execute the default target.

export const Target = new Juke.Target({
  // ...
});

export default Target;

Declare file inputs and outputs

If your target consumes and creates files, you can declare them on the target, so it would check whether it actually needs to rebuild.

If any input file is newer than the output file, target will be rebuilt, and skipped otherwise.

Supports globs.

export const Target = new Juke.Target({
  inputs: ['package.json', 'src/**/*.js'],
  outputs: ['dest/bundle.js'],
  // ...
});

Create parameters

Available parameter types are: string, number and boolean. You may add a [] suffix to the type to make it an array.

To provide a parameter via CLI, you can either specify it by name (e.g. --name), or its alias (e.g. -N). If parameter is not a boolean type, value will be expected, which you can provide via --name=value or -Nvalue.

To fetch the parameter's value, you can use the get helper, which is exposed on the target's context.

export const FileParameter = new Juke.Parameter({
  type: 'string[]',
  alias: 'f',
});

export const Target = new Juke.Target({
  executes: async ({ get }) => {
    const files = get(FileParameter);
    console.log('Parameter values:', files);
  },
  // ...
});

You can also dynamically set up target dependencies using binary expressions:

export const Target = new Juke.Target({
  dependsOn: ({ get }) => [
    get(FileParameter).includes('foo') && FooTarget,
  ],
  // ...
});

If you simply need access to arguments passed to the target, you can use the args context variable. Note, that you can only pass arguments that begin with - or --, because all other arguments are normally treated as targets to build.

export const Target = new Juke.Target({
  executes: async ({ args }) => {
    console.log('Passed arguments:', args);
  },
});

Context is available on these properties (when using a function syntax):

  • dependsOn
  • inputs
  • outputs
  • onlyWhen
  • executes

Notice: When referencing an unexported parameter, it must have a name, which is used in CLI for specifying the parameter.

const FileParameter = new Juke.Parameter({
  name: 'file',
});

Normally, name is derived from the name of the exported variable (minus the Parameter suffix, if it exists).

Conditionally run targets

If you need more control over when the target builds, you can provide a custom condition using onlyWhen. Target will build only when the condition is true.

Function can be async if it has to be, target will wait for all promises to resolve.

export const Target = new Juke.Target({
  onlyWhen: ({ get }) => get(BuildModeParameter) === BUILD_ALL,
  // ...
});

Execute an external program

Juke provides a handy Juke.exec helper.

export const Target = new Juke.Target({
  executes: async () => {
    await Juke.exec('yarn', ['install']);
  },
});

On program completion, you get its stdout and stderr. In case, when you need to run a program just to parse its output, you can set a silent option to stop it from piping its output to stdio.

const { stdout, stderr, combined } = await Juke.exec(command, ...args, {
  silent: true,
});

It throws by default if program has exited with a non-zero exit code (or was killed by a non-EXIT signal). If uncatched, error propagates through Juke and puts dependent targets into a failed state.

You can disable this behavior via:

const { code } = Juke.exec(command, ...args, {
  throw: false,
});

You can also simulate an exit code by rethrowing it yourself.

throw new Juke.ExitCode(1);

Run the build

You can build targets by specifying their names via CLI.

Every flag that you specify via CLI is transformed into parameters, and their names are canonically written in --kebab-case.

./build.js [globalFlags] task-1 [flagsLocalToTask1] task-2 [flagsLocalToTask2]

To specify an array of parameters, you can simply specify the same flag multiple times:

./build.js task-1 --foo=A --foo=B

You can also specify parameters via the environment. Environment variable names must be written in CONSTANT_CASE. If this parameter is an array, you can use a comma to separate the values.

FOO=A,B ./build.js task-1

Single target mode

You can specify that Juke CLI should only accept a single target to run.

Juke.setup({
  file: import.meta.url,
  singleTarget: true,
});

This mode means that all arguments after the first task name are considered as its arguments, regardless of whether they are flags or not.

./build.js [globalFlags] task [argsLocalToTask]

Various helpers

Juke.chdir(directory: string, relativeTo?: string)

Changes directory relative to another file or directory. Most commonly used as following:

Juke.chdir('..', import.meta.url);

Juke.rm(path: string, options = {})

Removes files and directories (synchronously). Supports a small subset of Node 16 options for fs.rmSync. Supports globs.

Juke.rm('**/node_modules', { recursive: true });

Juke.glob(pattern: string)

Unix style pathname pattern expansion.

Performs a search matching a specified pattern according to the rules of the glob npm package. Path can be either absolute or relative, and can contain shell-style wildcards. Broken symlinks are included in the results (as in the shell). Whether or not the results are sorted depends on the file system.

Returns a possibly empty list of file paths.

Examples

Our own build pipeline

/tg/station13 build pipeline

Screenshot image

License

Source code is available under the MIT license.

The Authors retain all copyright to their respective work here submitted.