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' })()
.Task: The Proxy'sgettrap is triggered. It stores "Task" as the current command name..create: The previous command ("Task") is pushed into storage, and "create" becomes the new active command.('Code review', ...): Theapplytrap is triggered. It takes the arguments and attaches them to the "create" command.(): 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.