npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hazae41/saumon

v0.2.14

Published

Ultra simple macro system for TypeScript

Downloads

159

Readme

Saumon 🐟

Ultra simple macro system for TypeScript

npm i @hazae41/saumon

Node Package 📦

Goals

  • Ultra simple and minimalist
  • Ultra fast thanks to Bun
  • 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 = $run$(() => fetch("/api/data").then(r => r.json()))

data.ts (output)

const data = { ... }

Compile-time code generation

log.macro.ts (input)

function $log$(x: string) {
  return `console.log("${x}")`
}

$log$("hello world")

log.ts (output)

console.log("hello world")

Usage

A macro is like a regular JS function, but the preprocessor will replace all its calls by the string value it returns

CLI

You need to install Bun

You can transform a single file

saumon build ./src/test.macro.ts

Or a whole directory

saumon build -r ./src

Files

The preprocessor will only transform files with .macro.* extensions

Definition

All macros must be named with one dollar before and one dollar after their name

function $log$(x: string) {
  return `console.log("${x}")`
}

Typing

You can spoof the returned type to avoid warnings while you code

function $random$(): number {
  return `${Math.random()}` as any
}
const x = $random$() * 100

In-file macro

You can define and call a macro in the same macro file

function $log$(x: string) {
  return `console.log("${x}")`
}

$log$("hello world")

Imported macro

You can export a macro (from any file) and import it in a macro file

log.ts

export function $log$(x: string) {
  return `console.log("${x}")`
}

main.macro.ts

import { $log$ } from "./log.ts"

$log$("hello from the main file")

You can even import macros from libraries

main.macro.ts

import { $log$ } from "some-lib"

$log$("hello from the main file")

You can also define a macro, export it, and use it in the same macro file

log.macro.ts

export function $log$(x: string) {
  return `console.log("${x}")`
}

$log$("hello from the log file")

main.macro.ts

import { $log$ } from "./log.ts"

$log$("hello from the main file")

Comment blocks

All comment blocks must start with /* or /** in the first line and @macro in the second line, and end with */

You can also inject arbitrary code in the comment block

This instruction will uncomment the given code and reparse the file

enabled.macro.ts

const enabled = true

/**
 * @macro uncomment
 * if (!enabled) {
 *   return exit(0)
 * }
 */

enabled.ts

const enabled = true

if (!enabled) {
   return exit(0)
}

You can use it to run macros in places where you are not supposed to call functions

something.macro.ts

function $log$(x: string) {
  return `log() {
    console.log("${x}")
  }`
}

class Something {

  /**
   * @macro uncomment
   * $log$("hello world")
   */

}

something.ts

class Something {

  log() {
    console.log("hello world")
  }

}

You can delete lines

This instruction will delete all the lines next to it until \n\n (or end of file)

/**
 * @macro delete-next-lines
 */
console.log("i will be deleted")
console.log("i will be deleted too")

You can use it to clean imports that are only used in macros

/**
 * @macro delete-next-lines
 */
import { $log$ } from "./macros/log.ts"
import { $hello$ } from "./macros/hello.ts"

$log$($hello$())
console.log("hello world")

Generic

You can use generic macro functions

parse.macro.ts

function $parse$<T>(x: string): T {
  return JSON.stringify(JSON.parse(x)) as any
}

export const data = $parse$<{ id: number }>(`{"id":123}`)

parse.ts

export const data = {"id":123}

Async

You can define and run async macros

Just return a Promise and the preprocessor will wait for it

fetch.macro.ts

function $fetch$<T>(url: string): T {
  return (async () => {
    const response = await fetch(url)
    const object = await response.json()

    return JSON.stringify(object)
  })() as any
}

export const data = $fetch$<{ id: number }>("https://dummyjson.com/products/1")

fetch.ts

export const data = { "id": 1 }

You can also await macroed code

function $f$(): Promise<number> {
  return `Promise.resolve(123)` as any
}

await $f$()

Dynamic

You can run dynamic code thanks to callbacks

function $run$<T>(callback: () => T): Awaited<T> {
  return (async () => {
    return JSON.stringify(await callback())
  })() as any
}
const data = $run$(() => fetch("/api/data").then(r => r.json()))

For your convenience, Saumon exports the $run$ macro so you can just import it

import { $run$ } from "@hazae41/saumon"

Constraints on in-file macro calls

Those constraints only apply when calling in-file macros, not when calling imported macros

Regular functions

When calling an in-file macro, it MUST be defined as a regular function

export const $log$ = function () {
  return `console.log("hey")`
}

export const $log$ = () => {
  return `console.log("hey")`
}

export function $log$() {
  return `console.log("hey")`
}

Top-level definition

When calling an in-file macro, it SHOULD be defined at top-level to avoid name conflicts

This is because the parser can't do code analysis to find which macro you want to use

function f() {

  function $log$() {
    return `console.log("hey")`
  }

  $log$()
}

function g() {

  function $log$() {
    return `console.log("hey")`
  }

  $log$()
}

function $log$() {
  return `console.log("hey")`
}

function f() {
  $log$()
}

function g() {
  $log$()
}

Local variables

When calling a macro in-file, variables MUST be primitive, global, or imported

This is because macro definitions and calls are ran isolated from their surrounding code

They can still access global variables and imports

❌ Calling an in-file macro that uses local variables

const debugging = true

function $debug$(x: string) {
  if (!debugging)
    return
  return `console.debug("${x}")`
}

$debug$("hey")

✅ Calling an in-file macro that uses global or imported variables

import { debugging } from "./debugging.ts"

function $debug$(x: string) {
  if (!debugging)
    return
  return `console.debug("${x}")`
}

$debug$("hey")

✅ Calling an imported macro

debug.ts

const debugging = true

export function $debug$(x: string) {
  if (!debugging)
    return
  return `console.debug("${x}")`
}

main.macro.ts

import { $debug$ } from "./debug.ts"

$debug$("hey")

Similarly, passed parameters MUST also be primitive, global, or imported (and their type too)

❌ Calling an in-file macro whose parameters are local

class X {}

function $log$(i: number, x: X) {
  return `console.log(${i}, "${JSON.stringify(x)}")`
}

$log$(123, new X())

✅ Calling an in-file macro whose parameters are imported

import type { X } from "./x.ts"
import { x } from "./x.ts"

function $log$(i: number, x: X) {
  return `console.log(${i}, "${JSON.stringify(x)}")`
}

$log$(123, x)

✅ Calling an imported macro

log.ts

export class X {}

export function $log$(i: number, x: X) {
  return `console.log(${i}, "${JSON.stringify(x)}")`
}

$log$(123, new X())

main.macro.ts

import { $log$, X } from "./log.ts"

$log$(123, new X())

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.