Compiler
The compilation pipeline uses esbuild with custom plugins to produce deployable infrastructure and runtime bundles.
Source: @notation/esbuild-plugins, @notation/core
Two-pass compilation
Notation compiles the project in two passes:
- Infrastructure pass — bundles the infra entry point with the
function-infra-plugin, producing the resource graph. - Function pass — bundles each
.fn.tsfile separately as a Node.js Lambda handler.
The infra pass determines what infastructure resources should be provisioned; the function produces the code artefacts that run on them.
The function-infra-plugin
The function-infra-plugin is an esbuild plugin that intercepts .fn.ts imports during the infrastructure build and replaces runtime handler code with infrastructure declarations.
The transformation
import { handle, json } from "@notation/aws/lambda.fn";
export const getTodos = handle.apiRequest(() => {
return json([{ id: 1, text: "Learn Notation" }]);
});
export const createTodo = handle.apiRequest(async (req) => {
const body = req.json();
return json({ id: 2, text: body.text });
});
export const config: LambdaConfig = {
service: "aws/lambda",
timeout: 5,
memory: 64,
};import { lambda } from "@notation/aws/lambda";
export const getTodos = lambda({
handler: "getTodos",
timeout: 5,
memory: 64,
});
export const createTodo = lambda({
handler: "createTodo",
timeout: 5,
memory: 64,
});How it works
- The plugin matches files with
.fnin the path - Parses the source to extract the
configexport and all named exports - Reads
config.serviceto determine the platform and service (e.g.,"aws/lambda") - Generates infrastructure code that imports the resource constructor and creates a resource for each export
- Reserved names (
preload,config) are skipped
Each named export in a .fn.ts file becomes a Lambda function (or other serverless resource). The same file defines both the runtime behaviour and the infrastructure required to run it.
Resource graph construction
After compilation, the resource graph is built by dynamically importing the compiled infrastructure module:
const mod = await import(outFilePath);
const register = mod.register ?? mod.default;
await register(collector);
return {
resourceGroups: collector.getResourceGroups(),
resources: collector.getResources(),
};The collector tracks resources as they're created during module execution. When lambda({ ... }) is called, it registers the Lambda function and its associated resources (IAM role, log group, zip package) with the collector.
The reconciler uses the resulting { resources, resourceGroups } object to plan deployments.
Watch mode compilation
In watch mode (notation watch), esbuild runs in context.watch() mode for both passes. Changes to source files trigger incremental rebuilds.
The CLI also watches for structural changes to the project:
- New
.fn.tsfiles — the function compiler is rebuilt to include them - Deleted
.fn.tsfiles — removed from the function compiler; orphaned resources are cleaned up by the reconciler - Config changes — trigger a re-evaluation of the infrastructure graph
Together this gives you a live development loop: save a file, and Notation recompiles, re-evaluates the graph, and deploys the diff.