@hazae41/saumon
v1.0.3
Published
Ultra simple macro system for TypeScript
Maintainers
Readme
Saumon 🐟
Ultra simple macro system for TypeScript
deno install -gf -RW npm:@hazae41/saumon --unstable-worker-optionsGoals
- 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
Example
Compile-time code evaluation
data.macro.ts (input)
const data = $$(() => fetch("/api/data").then(r => r.json()).then(JSON.stringify))data.ts (output)
const data = { ... }Compile-time code generation
log.macro.ts (input)
import { $$ } from "@hazae41/saumon"
$$(() => `console.log("hello world")`)log.ts (output)
console.log("hello world")Setup
Install Deno
npm install -g denoInstall Saumon with Deno
deno install -gf -RW npm:@hazae41/saumon --unstable-worker-optionsUsage
A macro is like a regular JS function, but the compiler will replace all its calls by the string value it returns
CLI
You can transform a single file
saumon ./src/test.macro.tsOr a whole directory
saumon ./src/**/**Files
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
Typing
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()}`) * 100Imports
You can import anything in a macro call
log.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")
})You can even import things from libraries
main.macro.ts
import { $$ } from "@hazae41/saumon"
$$(async () => {
const { $log$ } = await import("some-lib")
return $log$("hello world")
})Async
You can define and run async macros
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 }You can also await macroed code
promise.macro.ts
import { $$ } from "@hazae41/saumon"
const x = await $$<Promise<number>>(() => `Promise.resolve(123)`)promise.ts
const x = await Promise.resolve(123)Shared variables
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 abovePermissions
If 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
Security
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.
