Ultra simple macro system for TypeScript
npm install -D @hazae41/saumon- Ultra simple and minimalist
- Secured with Deno permissions
- Won't interfere with your existing tools
- Can output arbitrary code (TypeScript types, JSX components, JSON data)
- Resistant to supply-chain attacks
data.macro.ts (input)
const data = $$(() => fetch("/api/data").then(r => r.json()).then(JSON.stringify))data.ts (output)
const data = { ... }log.macro.ts (input)
import { $$ } from "@hazae41/saumon"
$$(() => `console.log("hello world")`)log.ts (output)
console.log("hello world")Install Deno and Saumon locally
npm install -D deno @hazae41/saumonWrite your script with Deno
"scripts": {
"generate": "deno x --unstable-worker-options saumon ./src/**/**"
}Run your script
npm run generateInstall Bun and Saumon locally
npm install -D bun @hazae41/saumonWrite your script with Bun
"scripts": {
"generate": "bun x --bun saumon ./src/**/**"
}Run your script
npm run generateInstall Deno globally
npm install -g denoInstall Saumon globally with Deno
deno install -gf -RW --unstable-worker-options npm:@hazae41/saumonRun Saumon with Deno
saumon ./src/**/**Install Deno globally
npm install -g denoRun Saumon with Deno
deno x --unstable-worker-options -RW npm:@hazae41/saumon ./src/**/**Install Bun globally
npm install -g bunRun Saumon with Bun
bun x --bun @hazae41/saumon ./src/**/**A macro is like a regular JS function, but the compiler will replace all its calls by the string value it returns
You can transform a single file
saumon ./src/test.macro.tsOr a whole directory
saumon ./src/**/**The compiler will only transform files with .macro.* extensions
This is good for performances because it won't parse all your code
And this is good for security because it will only run code in there
All macros must be called with $$(() => Awaitable<string>)
export declare function $$<T>(f: () => Awaitable<string>): TYou can spoof the returned type to avoid warnings while you code
const x = $$<number>(() => `${Math.random()}`) * 100log.ts
export function $log$(x: string) {
return `console.log("${x}")`
}main.macro.ts
import { $$ } from "@hazae41/saumon"
$$(async () => {
const { $log$ } = await import("./log.ts")
return $log$("hello world")
})main.macro.ts
import { $$ } from "@hazae41/saumon"
$$(async () => {
const { $log$ } = await import("some-lib")
return $log$("hello world")
})Just return a Promise and the compiler will wait for it
fetch.macro.ts
import { $$ } from "@hazae41/saumon"
const data = $$(() => fetch("https://dummyjson.com/products/1").then(r => r.json()).then(JSON.stringify))fetch.ts
export const data = { "id": 1 }promise.macro.ts
import { $$ } from "@hazae41/saumon"
const x = await $$<Promise<number>>(() => `Promise.resolve(123)`)promise.ts
const x = await Promise.resolve(123)When calling a macro, in-file local variables are NOT accessible
This is because macro calls are ran isolated from their surrounding code (in a worker)
They can still access imports, so you can put shared things in some file, and/or pass them
x.ts
export const x = Math.random()main.macro.ts
import { $$ } from "@hazae41/saumon"
const x = $$<number>(async () => {
const { x } = await import("./x.ts")
console.log(`x is ${x}`)
return `${x}`
})
console.log(`x is ${x}`) // exact same as aboveIf you use Deno as your runtime, you can benefit from it's permissions based-security
$ deno run -RW ./src/bin.ts ./test/fetch.macro.ts
┏ ⚠️ Deno requests net access to "dummyjson.com:443".
┠─ Requested by `fetch()` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-net
┠─ Run again with --allow-net to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) > y✅ Granted net access to "dummyjson.com:443".Each macro call will have its own independent permissions
So when you type A it's always within the same macro call
Macro files are transformed ahead-of-time by the developer.
This means the output code is fully available in the Git, and won't interfere with code analysis tools.
The macro code SHOULD only be transformed when needed (e.g. when modified, when the fetched data is stale), and its output SHOULD be verified by the developer.
The developer SHOULD also provide the input macro file in the Git, so its output can be reproducible by people and automated tools.