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

@elg/tscodegen

v0.5.0

Published

TypeScript string-based code generation tool, with hashed files and manually editable sections

Downloads

4,819

Readme

tscodegen

CircleCI codecov Maintainability

tscodegen is a minimal TypeScript port of Facebook's Hack Codegen. It provides a fluent API that allows you to build human-readable TypeScript source files from strings of code, with optional manually editable sections and tamper detection.

Key Features

  • Manually editable sections within generated files. This behavior is nearly identical to Hack Codegen's amazing partially generated files. Unlike most other code generation tools, tscodegen (and Hack Codegen) is able to produce editable files, allowing your codegen output to be easily customized and extended without monkeypatching or subclassing. These edits will be preserved when the code is regenerated.

  • String-based code generation. We sidestep dealing with an AST (e.g. with ts-morph or the TypeScript compiler API) in favor of string manipulation. This lets you write your generated code naturally, without having to know the details of TypeScript's AST.

  • Codelock tamper detection. A hash is added to the docblock of every generated file to make it easy to detect if a file has been changed outside the generated manual sections. In the future, this can be enforced with an ESLint rule (PRs welcome!).

  • No template files. tscodegen uses template literals and TypeScript method calls, allowing you to use loops and conditionals without having to learn another templating language.

  • Auto formatting with Prettier, using your project's Prettier config. Generated code has rarely been so readable.

  • Clean, fluent, minimal API.

Installation

npm install @elg/tscodegen prettier # NPM
yarn add @elg/tscodegen prettier # Yarn

Usage

  1. import { CodeFile } from '@elg/tscodegen';
  2. Instantiate CodeFile.
  3. Call build to build the file with a CodeBuilder instance.
  4. Write the built file to disk with saveToFile.

Sample

Codegen script

new CodeFile("./file.ts")
  .build((builder) =>
    builder
      .addLine("import path from 'path';")
      .addLine("import fs from 'fs'")
      .addLine()
      .addManualSection("custom_imports", (builder) => builder)
      .addLine()
      .addBlock("class Steam extends Water", (builder) =>
        builder
          .addBlock("constructor()", (builder) =>
            builder.addLine("this.boil();"),
          )
          .addLine()
          .addBlock("boil()", (b) =>
            b.addManualSection("boil_body", (builder) =>
              builder.add("this.temp = 100;"),
            ),
          ),
      )
      .format(),
  )
  .lock()
  .saveToFile();

Output

/**
 * This file is generated with manually editable sections. Only make
 * modifications between BEGIN MANUAL SECTION and END MANUAL SECTION
 * designators.
 *
 * @generated-editable Codelock<<jF8gPj9IVq16NXBAtEzJj0rrD9HR7Q6V>>
 */

import path from "path";
import fs from "fs";

/* BEGIN MANUAL SECTION custom_imports */
/* END MANUAL SECTION */

class Steam extends Water {
  constructor() {
    this.boil();
  }

  boil() {
    /* BEGIN MANUAL SECTION boil_body */
    new Magician().magic(); // This line was retrieved from the original file.ts
    /* END MANUAL SECTION */
  }
}

Generating non-TypeScript files

By default, CodeFile emits JSDoc/C-style docblocks and manual section markers. Pass a commentSyntax option to target file formats that only support line comments (e.g. .gitattributes with # comments, shell scripts, or a .ts file where you prefer // docblocks):

export type CommentSyntax =
  | { kind: "jsdoc" } // default — JSDoc/C-style
  | { kind: "line"; prefix: string }; // e.g. "# " or "// "

Example .gitattributes generator:

new CodeFile(".gitattributes", {
  commentSyntax: { kind: "line", prefix: "# " },
})
  .build((b) =>
    b
      .addManualSection("manual", (m) => m.addLine("# add custom rules here"))
      .addLine("path/to/generated.ts linguist-generated=true"),
  )
  .lock("\nTo update this file, run: npm run generate:gitattributes\n")
  .saveToFile();

Sample output:

# This file is generated with manually editable sections. Only make
# modifications between BEGIN MANUAL SECTION and END MANUAL SECTION
# designators.
#
# To update this file, run: npm run generate:gitattributes
#
# @generated-editable Codelock<<...>>

# BEGIN MANUAL SECTION manual
# add custom rules here
# END MANUAL SECTION
path/to/generated.ts linguist-generated=true

Everything else works the same: verify() detects tampering outside the manual sections, lock() adds the hash, and saveToFile() writes the result. Manual-section keys still must be non-empty and whitespace-free.

Generating indentation-sensitive languages (Python, YAML, Makefile, …)

For languages where indentation is syntactic, use CodeBuilder.indent(amount, fn) to open a nested scope in which every emitted line (including manual-section markers and bodies) is prefixed with amount. Indent scopes compose additively: indent(" ", b => b.indent(" ", bb => …)) produces four spaces of indent.

new CodeFile("compute.py", { commentSyntax: { kind: "line", prefix: "# " } })
  .build((b) =>
    b
      .addLine("def compute(x: int) -> int:")
      .indent("    ", (fn) =>
        fn
          .addLine("result = x + 1")
          .addManualSection("postprocess", (m) => m.addLine("return result")),
      ),
  )
  .lock()
  .saveToFile();

Output:

# This file is generated with manually editable sections. Only make
# modifications between BEGIN MANUAL SECTION and END MANUAL SECTION
# designators.
#
# @generated-editable Codelock<<...>>

def compute(x: int) -> int:
    result = x + 1
    # BEGIN MANUAL SECTION postprocess
    return result
    # END MANUAL SECTION

Manual sections round-trip cleanly across regenerations: the stored body is semantic (column-0 content), and the builder reapplies the ambient indent on the way out. So a human who edits the section and adds lines at the matching indent:

    # BEGIN MANUAL SECTION postprocess
    if result > 100:
        raise ValueError(result)
    return result
    # END MANUAL SECTION

will see their edits preserved verbatim after regeneration, with no re-shifting of nested indents (the if/raise pair keeps its 4-space relative indent).

indent() accepts any string — typically spaces or "\t" — so it also covers Makefile recipes, YAML mappings, Terraform/HCL blocks, INI-like formats, or any other layout where column position matters.

Supported file types by CommentSyntax

tscodegen treats the comment prefix as an opaque string, so any language whose comments fit one of the supported shapes works out of the box. The table below lists the configurations for common target file types.

| File type | CommentSyntax | | ----------------------------------- | --------------------------------------------- | | TypeScript / JavaScript / TSX / JSX | { kind: "jsdoc" } (default) | | Go, Rust, Swift, Kotlin, Java | { kind: "jsdoc" } | | C, C++ | { kind: "jsdoc" } | | CSS / SCSS / LESS | { kind: "jsdoc" } | | Protobuf | { kind: "jsdoc" } | | TS/JS (line-comment preferred) | { kind: "line", prefix: "// " } | | Python, Ruby | { kind: "line", prefix: "# " } | | Shell / Bash / zsh | { kind: "line", prefix: "# " } | | YAML, TOML | { kind: "line", prefix: "# " } | | Dockerfile, .env | { kind: "line", prefix: "# " } | | Terraform / HCL, GraphQL | { kind: "line", prefix: "# " } | | Makefile | { kind: "line", prefix: "# " } + tab indent | | .gitattributes, .gitignore | { kind: "line", prefix: "# " } | | nginx.conf, systemd unit | { kind: "line", prefix: "# " } | | SQL | { kind: "line", prefix: "-- " } |

The Python/YAML/Terraform/Makefile/SQL snapshots in src/__snapshots__/integration.test.ts.snap are a living catalogue of what generated output looks like for each of these.

Known limitations

  • JSON has no comment syntax, so it cannot be locked. If you need to generate JSON alongside other files, either emit a companion metadata file or use JSONC ({ kind: "line", prefix: "// " }).
  • XML / HTML / SVG / Markdown / Vue SFCs / MDX use <!-- ... --> wrapping comments, which is not expressible in CommentSyntax.
  • Shebangs must be line 1. lock() prepends its docblock, so a locked shell script's #!/usr/bin/env bash ends up below the docblock. For scripts invoked as executables, either prepend the shebang yourself after locking or invoke them via the interpreter directly.