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

@trut/gen

v0.2.0

Published

A modular toolkit for snippet-based code generation. Build and customize your own scaffolding system.

Readme

Gen

A modular toolkit for snippet-based code generation. Define generators once and run them interactively or from automation with the same inputs.

Install

pnpm add -D @trut/gen

Quick start

Create gen.config.ts at your project root. It points at the glob patterns Gen uses to discover generator modules:

import { defineConfig } from '@trut/gen';

export default defineConfig({
  generators: ['./generators/*.generator.ts'],
});

Add a generator module. A discovered file can export one generator, an array of generators, or an object with a generators array:

import { addTemplates, defineGenerator, text, updateExports } from '@trut/gen';

export default defineGenerator({
  name: 'component',
  description: 'Create a component and update the barrel export',
  basePath: 'src',
  prompts: {
    name: text({ message: 'Component name', placeholder: 'UserProfile' }),
  },
  actions: [
    addTemplates({
      from: 'templates/component',
      to: 'components/<%= it.format.kebabCase(it.answers.name) %>',
    }),
    updateExports({
      file: 'components/index.ts',
      from: './<%= it.format.kebabCase(it.answers.name) %>',
      members: ['<%= it.format.pascalCase(it.answers.name) %>'],
    }),
  ],
});

Add the templates next to the generator. from resolves relative to the module that defines the generator, so templates/component lives beside it in generators/. The file name inside the braces is a placeholder; see Filesystem name placeholders.

generators/templates/component/{pascalCase:name}.tsx.eta:

export const <%= it.format.pascalCase(it.answers.name) %> = () => <div />;

generators/templates/component/index.ts.eta:

export * from './<%= it.format.kebabCase(it.answers.name) %>';

Create the barrel that the generator updates, src/components/index.ts:

// Components barrel

Run the generator with every value supplied as a flag:

pnpm gen component --name "User Profile"

Because every prompt value is supplied, the command runs without prompting, writes the component tree below src/components/user-profile, and appends the export to src/components/index.ts:

// Components barrel
export { UserProfile } from './user-profile';

Run pnpm gen component from a terminal instead to be prompted for the name.

Configuration

defineConfig takes an object with a generators array of glob patterns. Gen expands each pattern from the directory that contains gen.config.*, imports every matching module, and flattens its exports into a list of generators. A module may default-export one generator, an array of generators, or an object with a generators array, and may also name-export generators; nested arrays are flattened. Modules that export no generator are ignored, and a generator reached through more than one pattern or re-export is loaded once. Two distinct generators that share a name fail the run.

templateAdapter is optional. Without it, Gen renders Eta templates. Set it to choose another renderer and template-file convention; see Template adapters.

defineGenerator returns a generator with:

  • name (required): the CLI command name. The file name never affects it.
  • description (optional): shown in the generator selection list.
  • basePath (required): a static path, resolved from the directory that contains gen.config.*. Every file a generator writes stays below it.
  • prompts (optional): a map of input name to prompt definition.
  • actions (required): the ordered list of changes to stage.

Prompts and CLI flags

Each prompt definition produces both a Clack interaction and a CLI flag. The flag is the kebab-case form of the prompt key (componentName becomes --component-name). A supplied flag value is parsed and validated by the same prompt contract and skips the interaction.

| Constructor | Value | CLI flag | | ------------- | -------- | ----------------------------------- | | text | string | --name <value> | | password | string | --token <value> | | number | number | --count <value> | | confirm | boolean | --enabled / --no-enabled | | select | string | --framework <value> | | multiselect | string[] | --tags a --tags b or --tags a,b |

Common options: message, description, alias (short flag), default, validate, and format. A default is shown during an interactive run and used for a missing value in a non-interactive run. A missing value with no default fails and names the CLI flag.

Global flags:

  • --dry-run: report the files that would be written without writing them.
  • --force: replace existing files that collide with generated output.
  • -h, --help: print usage.
  • -v, --version: print the version.

Actions

addTemplates({ from, to })

Recursively copies the from directory (relative to the module that defines the generator) into to (relative to the generator basePath). With the default Eta adapter, a file ending in .eta is rendered and written without that final extension; other files are copied verbatim. See Template adapters to use another engine or file convention.

updateImports({ file, from, members, typeOnly? })

Merges the named members into the import declaration for the from module in file (relative to the basePath) and alphabetizes the members. Supported files are .ts, .tsx, .js, and .jsx. Existing declarations keep their order, and side-effect-only imports are never merged or moved. A missing module declaration is added as a new import after the existing imports.

updateExports({ file, from, members, typeOnly? })

Merges the named members into the export declaration for the from module and alphabetizes them while preserving the order of existing export declarations. Adding the same member twice is a no-op.

Extending actions

The action set is extensible. Declare a new action shape by augmenting the GenActions interface, then register a handler for its type. Custom handlers receive the same run context as the built-in ones, so they can read answers, read content staged earlier in the run, and stage output into the same commit.

import { join } from 'node:path';
import { addTemplates, defineGenerator, type ActionHandlerRegistry } from '@trut/gen';
import { runGenerator, stageFile } from '@trut/gen/runtime';

declare module '@trut/gen' {
  interface GenActions {
    'acme/manifest': { readonly type: 'acme/manifest'; value: string };
  }
}

const generator = defineGenerator({
  name: 'service',
  basePath: 'services/<%= it.answers.name %>',
  actions: [
    addTemplates({ from: 'templates/service', to: '.' }),
    { type: 'acme/manifest', value: 'acme' },
  ],
});

const handlers: Partial<ActionHandlerRegistry> = {
  'acme/manifest': (action, context) => {
    stageFile(
      context,
      join(context.basePath, 'manifest.json'),
      JSON.stringify({ value: action.value }),
    );
  },
};

await runGenerator({ generator, configDir, moduleDir, answers, handlers });

Supplied handlers are merged over the built-in handlers, so registering a custom handler keeps every built-in action runnable. Namespace the action key per tool (acme/manifest) so two wrappers that augment the same program do not collide.

Templates

By default, generator configuration strings and .eta file contents use standard Eta delimiters. The render context exposes:

  • it.answers: the resolved prompt values, keyed by prompt name.
  • it.format: the change-case formatters (pascalCase, kebabCase, camelCase, snakeCase, and more).
'<%= it.format.pascalCase(it.answers.name) %>';

Set templateAdapter in defineConfig to choose another rendering syntax and template-file convention. It renders template contents and dynamic action strings. See Template adapters for the adapter contract and examples.

Filesystem name placeholders

Directory and file names inside a from template tree expand two placeholder forms before writing:

  • {name} inserts the raw input value.
  • {formatter:name} applies a change-case formatter, for example {pascalCase:name}.tsx.eta becomes UserProfile.tsx.

A placeholder with no matching input or an unknown formatter fails the run before anything is committed.

Safe writes

All generated files and source updates are staged first and written together at the end of the run. Output paths are normalized and rejected if they escape the generator basePath. An existing file collides and fails the run unless --force is supplied. With --dry-run the staged set is reported and nothing is written.

Programmatic usage

Import the engine from @trut/gen/runtime to run generators from another tool. The root (@trut/gen) stays free of runtime dependencies and exports the shared types, so a project that only declares generators loads no engine.

import { addTemplates, defineGenerator, text, type PromptAdapter } from '@trut/gen';
import { loadGenConfig, resolveGenerator, resolvePrompts, runGenerator } from '@trut/gen/runtime';

const generator = defineGenerator({
  name: 'component',
  basePath: 'src',
  prompts: { name: text({ message: 'Component name' }) },
  actions: [addTemplates({ from: 'templates/component', to: '.' })],
});

// Run a code-defined generator without a gen.config.* file. `configDir`
// anchors `basePath`; `moduleDir` anchors template `from`.
await runGenerator({
  generator,
  configDir: process.cwd(),
  moduleDir: process.cwd(),
  answers: { name: 'Button' },
});

// Or drive the config-discovery pipeline yourself with a PromptAdapter.
const adapter: PromptAdapter = createMyAdapter();
const loaded = await loadGenConfig(process.cwd());
const resolved = await resolveGenerator({
  generators: loaded.generators,
  name: 'component',
  prompts: adapter,
});
const answers = await resolvePrompts({
  prompts: resolved.generator.prompts,
  values: { name: 'Button' },
  adapter,
});
await runGenerator({
  generator: resolved.generator,
  configDir: loaded.configDir,
  moduleDir: resolved.moduleDir,
  answers,
});

@trut/gen/runtime also exposes the loader, resolver, hooks, staging and rendering primitives, the built-in action handlers, and the AST editors (editImports/editExports). The CLI entry and its Clack adapter stay in @trut/gen/cli, so a non-interactive consumer never loads the prompt library.