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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sututu/transporter

v0.1.1

Published

Transport manager for sending messages through different transports

Readme

@sututu/transporter

Transport manager for sending messages through different services/protocols.

Installation

Add the package to your app:

npm install @sututu/transporter

For SMTP email transport, also install optional peer dependency:

npm install nodemailer

Mailjet transport uses HTTP API and does not require additional dependencies.

Configuration

Define a top-level transporter section in sututu.config.js:

export default {
  transporter: {
    default: 'email',
    transports: {
      email: {
        transport: 'email',
        from: 'Sututu Notifications <[email protected]>',
        templatePath: './mail/templates',
        layoutPath: './mail/layouts',
        smtp: {
          host: 'localhost',
          port: 1025,
          secure: false
        }
      },
      echo: {
        transport: 'echo',
        from: 'Sututu Notifications <[email protected]>',
        templatePath: './mail/templates',
        layoutPath: './mail/layouts'
      },
      mailjet: {
        transport: 'mailjet',
        from: 'Sututu Notifications <[email protected]>',
        templatePath: './mail/templates',
        layoutPath: './mail/layouts',
        apiKey: process.env.MAILJET_API_KEY,
        apiSecret: process.env.MAILJET_API_SECRET,
        sandbox: true
      }
    }
  }
}

The from field supports mailbox format with display name, for example: John Doe <[email protected]>.

If templatePath is not set, the transporter reads templates from ./mail/templates by convention. If layoutPath is not set, the transporter reads layouts from ./mail/layouts by convention.

For template default-email, expected files are:

  • default-email.html (required)
  • default-email.txt (optional; if missing, email is sent without multipart text body)

For layout default-layout, expected files are:

  • default-layout.html (required)
  • default-layout.txt (optional)

Layout files must include exactly one outlet: {{content}}.

Usage

import { createTransporter } from '@sututu/transporter'

const transporter = await createTransporter()

await transporter.send({
  to: '[email protected]',
  subject: 'Welcome Ana',
  template: 'default-email',
  layout: 'default-layout',
  variables: {
    name: 'Ana',
    content: 'Your account is ready'
  }
})

// pick transport explicitly with chainable API
await transporter.with('email').send({
  to: '[email protected]',
  subject: 'Welcome Ana',
  template: 'default-email',
  layout: 'default-layout',
  variables: {
    name: 'Ana',
    content: 'Your account is ready'
  }
})

// Echo transport prints the fully rendered email in server logs
await transporter.with('echo').send({
  to: '[email protected]',
  subject: 'Debug email',
  template: 'default-email',
  layout: 'default-layout',
  variables: {
    name: 'Ana',
    content: 'This is a debug echo message'
  }
})

// Mailjet transport sends rendered template content using Mailjet Send API
await transporter.with('mailjet').send({
  to: '[email protected]',
  subject: 'Welcome Ana',
  template: 'default-email',
  layout: 'default-layout',
  variables: {
    name: 'Ana',
    content: 'Your account is ready'
  }
})

Custom transports

Built-in transport types are email, echo, and mailjet. To use a transport type that isn't one of these (SMS, Slack, WhatsApp, a different email API), add a custom transport from your own app — no fork of @sututu/transporter required.

Folder convention

src/
  transports/   # each file exports { type, transport }

Every .js file directly inside src/transports/ (non-recursive) is loaded. A file that default-exports { type: string, transport: Function } is registered as a new transport type; any other file in that folder is silently skipped, not an error.

Example

// src/transports/slack.js
import { BaseTransport } from '@sututu/transporter/transports/BaseTransport'

class SlackTransport extends BaseTransport {
  constructor(config = {}, dependencies = {}) {
    super(config)
    this.fetchImpl = dependencies.fetchImpl || fetch
  }

  async connect() {
    // No persistent connection needed for a webhook-based transport.
  }

  async send(message = {}) {
    if (!message.text) throw new Error('Slack transport requires `text`')

    const response = await this.fetchImpl(this.config.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: message.text, channel: message.channel || this.config.defaultChannel })
    })

    if (!response.ok) throw new Error(`Slack transport failed: ${response.status}`)
    return { delivered: true }
  }

  async disconnect() {}
}

export default { type: 'slack', transport: SlackTransport }
export default {
  transporter: {
    default: 'slack',
    transports: {
      slack: { transport: 'slack', webhookUrl: process.env.SLACK_WEBHOOK_URL }
    }
  }
}
await transporter.send({ text: 'Deploy finished ✅' })

Extend EmailTransport instead of BaseTransport to reuse the built-in template/layout rendering for a new provider.

How discovery works

TransporterManager scans src/transports/ as the first step of connectAll(), before any configured transport connects:

  1. Built-in types (email, echo, mailjet) are registered.
  2. The app's src/transports/ folder is scanned; each valid { type, transport } export is registered on top of the built-ins.
  3. Configured transports are created, resolving transport: '<type>' against the now-complete set of types.

A custom type that collides with a built-in name overrides the built-in — pick a distinct name unless that's intentional.

The folder location is fixed — src/transports is not configurable — so every app using @sututu/transporter follows the same convention.

API

  • createTransporter(options)
  • createTransporterManager(config)
  • TransporterManager#send(message, transportName?)
  • TransporterManager#with(transportName).send(message)
  • TransporterManager#registerAdapter(type, TransportClass)
  • TransporterManager#registerTransport(name, transport)
  • TransporterManager#getTransport(name?)