Skip to content

Latest commit

 

History

History
187 lines (135 loc) · 5.48 KB

full.mdx

File metadata and controls

187 lines (135 loc) · 5.48 KB

Atbuild Full

Atbuild does not parse JavaScript and is not a fork of JavaScript.

It's more like a templating language for generating code that happens to execute & bundle JavaScript. Theoretically, someone could use Atbuild for other languages with few modifications.

In this templating language, there are four keywords. All other text is assumed to be part of the parent scope. From the language's perspective, if the text isn't @build, @run, @(), @end, or @export function, like Jon Snow, it knows nothing.

  • @build: execute code at build-time, do not include it in the runtime code.
  • @run: execute code at run-time
  • @(code): Execute code in opposite scope (parent is @build? It means @run. Parent is @run? It means @build)
  • @export function: Export a build-time function that can be used in Atbuild Light (inside regular JavaScript/Typescript files)

Lets dive right in with an example that shows many features altogether.

// This exports a function that returns a date time formatter.
@export function $createDateTimeFormatter(format) {
  // By default, everything inside @run is executed at runtime.
  @run
    function (date) {
      let formattedDate = "";

      // This changes the default, so everything inside @build is executed at build time.
      @build
        for (let i = 0; i < format.length; i++) {
          let lookAheadCount = 0;

          for (lookAheadCount = 0; lookAheadCount + i < format.length && format[lookAheadCount + i] === format[i]; lookAheadCount++) {
          //  Merge repeated characters
          //  So if i = 0 and format = "hh:mm:ss"
          //  This becomes "hh"
          }
          let token = tokens.substring(i, i + lookAheadCount + 1);


          if (formatters[token]) {
            // formatters would be defined elsewhere in the same file, but is too big to show an example here.
            const tokenFormatter = formatters[token];

            // Now we switch back to runtime
            @run
                            // @(code) is shorthand for "run at opposite scope"
                            // so this runs at build-time.
              formattedDate += @(tokenFormatter(token))
            @end
          }

          i += lookAheadCount
        }
      @end

      return formattedDate;
    }
  @end
@end

@build & @run

Most of the time, you'll want multiline sections. A multiline section starts with a keyword (@build, @run, or @export) and ends with @end.

@build
  const buildTimeOnly = true;
  const isMultilineScope = true;
@end

That compiles into this, which is executed at buildtime.

const buildTimeOnly = true;
const isMultilineScope = true;

You can nest @build and @run infinitely, and use @build or @run on a line by itself:

@build
  for (let i = 0; i < 4; i++) {
    @run (console.log("Hey!");)
    //   ^-------------------^
    //   Everything between the parentheses is added to the source code at runtime.
    //   Including the ;
  }
@end

That compiles into this:

console.log("Hey!");
console.log("Hey!");
console.log("Hey!");

Sometimes it gets messy typing @build or @run all over the place, particularly if you're in the middle of something complicated.

For shorter syntax, use @(). The rule for @()is:

  • If the parent section is @build, then it means @run.
  • If the parent section is @run, then it means @build.

Here's an example:

@export function $BitField(object, _BitFieldClassName)
  @build
    const BitFieldClassName = _BitFieldClassName || "BitFieldClassName"
    const BitFieldMixin = require("structurae").BitFieldMixin;
    const { fields, masks, schema, offsets } = BitFieldMixin(object);

    @run
      function() {
        return class @(BitFieldClassName) {
                //     ^----------------^
                //   The value of BitFieldClassName is replaced
                //



                // ...More code omitted
        }
      }
    @end

  @end
@end

@export

@export lets you export functions that're replaced at build-time.

This enables runtimeless libraries.

Inside a .tsb/.jsb file:

@export function $getBuildTimestamp(format)
  @build
    const timestamp = new Date();
    @run
      new Date("@(timestamp.toISOString())"))
    @end
  @end
@end

And from a normal .ts/.js file:

// $$

import { $getBuildTimestamp } from "./getBuildTimestamp";

// $$-

export const LAST_UPDATED_AT = $getBuildTimestamp();

At runtime, this becomes:

export const LAST_UPDATED_AT = new Date("2020-11-07T10:01:00.300Z");

The rules are:

  • Exported function names must start with a $
  • Exported functions must not have curly braces { or } at the beginning/end.

How this actually works

This might seem a little magical, so lets dive in to how this works.

In src/light.ts, there's a lightweight AST which goes through JavaScript & TypeScript files searching for a few patterns:

  • $(expression-in-here)
  • $FunctionNameThatStartsWithADollarSign(expression-in-here)
  • // $
  • // $$

When it finds a match, it reads & runs the build-time code, and swaps the original source with the result from the build-time generated code.

For performance reasons, this is not done through Babel. It's simple enough that it can get away with not parsing all of JavaScript's syntax. And, it also does one scan to check for any "$" in the file (and if not, skip the file).