Inspired by Paul Graham's “Beating the Averages” and the expressive power of Lisp macros, the dsl-framework was born from a desire to treat JavaScript code as a malleable data structure without relying on string manipulation or complex build-time annotations.

The "Git Macro" Philosophy

In Lisp, macros allow you to transform code before it executes. In JavaScript, we don't have a native macro system, but we do have Proxies. dsl-framework utilizes the Proxy object to intercept property access and function calls, effectively building an Abstract Syntax Tree (AST) in real-time as you type.

How it Works: The Proxy Engine

At the heart of the framework lies the caller Proxy (found in src/core/index.js). Every time you "dot" into a new command or "invoke" a function in the chain, the framework doesn't execute logic—it records intent.

const caller = new Proxy(callerRaw, {
  get (obj, prop) {
    // Intercepts .commandName
    state.setCommandName(prop);
    return caller; // Returns itself to allow infinite chaining
  },
  apply (target, thisArg, argumentsList) {
    // Intercepts ('arguments')
    return target(...argumentsList);
  }
});

The Execution Trigger: The Closing ()

Unlike traditional fluent interfaces (like jQuery), where each method immediately returns a modified version of the original object or triggers an action, dsl-framework treats the entire chain as a Sequence. It silently builds up state until you tell it to stop.

How do you tell it you're finished? By invoking the chain with an empty function call: ().

First-Class Sequences: The pendingCommand

One of the most powerful features of this architecture is that the chain remains a live, transferable object until that final invocation. We call this a pendingCommand.

Because the Proxy keeps returning itself, you can pass a partial command across different modules, layers, or functions. This allows for incredibly terse code where different parts of your application "fill in" the data before the final execution.

// 1. Start a command in one part of your app
const pendingCommand = dsl().Task.create('Code review');

// 2. Pass it around! You can add more context later
function addMetadata(cmd) {
  return cmd.priority('high').tag('urgent');
}

const enrichedCommand = addMetadata(pendingCommand);

// 3. The closing () finally triggers the execution
const result = enrichedCommand(); 

This "lazy execution" turns your DSL into a first-class citizen. You aren't just calling functions; you are constructing a command object through syntax.

The Evolution of a Command

Consider the sequence: .Task.create('Code review', { due: '2023-10-05' })()

  1. .Task: The Proxy's get trap is triggered. It stores "Task" as the current command name.
  2. .create: The previous command ("Task") is pushed into storage, and "create" becomes the new active command.
  3. ('Code review', ...): The apply trap is triggered. It takes the arguments and attaches them to the "create" command.
  4. (): The empty invocation triggers the end of the sequence, packaging everything into a readable data structure.

The Parser: Code as Data

The command-parser (in src/core/command-parser/index.js) provides the framework's "Macro" capabilities. Once the chain is executed, it allows you to query the structure of the sequence using logical operators:

  • .hasAnd('Task', 'Deadline'): Ensures the sequence contains both required elements before proceeding.
  • .arguments('Task', 'firstArgument'): Extracts specific data points without manual array indexing.

A Different Kind of Flexibility

Traditional JavaScript APIs often require specific parameters in a strict order. If you need to add a parameter, you change the function signature. If you need to change the order, you refactor every call site.

With dsl-framework, you are building a Domain Specific Language. The logic is entirely decoupled from the syntax. You can evolve your software naturally, adding new "keywords" to your DSL without breaking existing integrations. It provides a way to treat your code as pure data, allowing you to design APIs that adapt to your domain, rather than forcing your domain to adapt to rigid functions.