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

@temir.ra/create-ts-lib

v0.13.5

Published

A template for a distributable TypeScript library package.

Readme

Introduction

A template for TypeScript libraries distributed via npm-compatible registries. Provides TypeScript configuration, build tooling for ESM and bundled outputs, and build metadata generation.

Table of Contents

  1. Quick Start
  2. Documentation
    1. package.json
    2. Script scripts/buildinfo.ts
    3. Script scripts/build-bundle.ts
      1. Import Map scripts/import-map.json
    4. tsconfig.build.json
    5. Package Files Resolution
      1. Accessing package files at runtime
    6. "bin" field in package.json
  3. DevOps
    1. Change Management
    2. Publish

Quick Start

# placeholder:
    # <TEMPLATE_PACKAGE_NAME: @temir.ra/create-ts-lib
    # <TEMPLATE_NAME: @temir.ra/ts-lib

# print the latest version
npm info "@temir.ra/create-ts-lib" version

# create/update a package from the template in the current directory
npm create --no-install --no-git "@temir.ra/ts-lib@latest" .

# set metadata in package.json

npm update

Documentation

The following sections explain the configurations and conventions baked into the generated package. Useful when adapting it to fit specific needs.

The major addition compared to the workspace template is a build pipeline for distributing the library. Two build strategies are supported:

  • Bundling (scripts/build-bundle.ts) - bundles the library to ESM and IIFE formats using esbuild.
  • TSC compilation (tsconfig.build.json + build:tsc) - compiles the library to declaration files, and optionally ESM JavaScript, using tsc.

Both strategies can be combined.

package.json

Selected fields are documented in the workspace template README.

The following fields are specific to this template:

{

  // ... ,

  // the package is anticipated to be published
  "private": false,

  // ... ,

  // treats all .js files as ES modules; use .cjs extension for CommonJS files
  "type": "module",

  // files to include in the published package
  "files": [
    "dist/"
  ],

  // CLI entry point
  "bin": "./dist/cli.bundle.js",

  // package entry points
  // multiple entry points can be configured (".", "./module/", etc.)
  // 
  // scripts/build-bundle.ts (non-standard) export condition:
  // "entrypoint" - locates the source entry point for bundling
  // 
  // standard export conditions:
  // "types"      - TypeScript consumers; resolves to the declaration files;
  // "browser"    - browser bundler consumers; resolves to the bundled output
  // "import"     - ESM consumers; resolves to the bundled/compiled output
  "exports": {
    ".": {
      "entrypoint": "./src/index.ts",
      "types": "./dist/index.d.ts",
      "browser": "./dist/index.bundle.js",
      "import": "./dist/index.bundle.js"
    }
  },

  // convenience alias for source-execution only
  // does NOT survive transpilation or bundling
  "imports": {
    "#src/*.js": "./src/*.ts"
  },

  "scripts": {

    // ... ,

    // generates buildinfo.txt with version + git hash
    "buildinfo": "tsx scripts/buildinfo.ts",

    // removes the dist/ directory generated by the build steps
    "clean:dist": "rm -rf dist/",

    // removes .tsbuildinfo files generated by TypeScript's incremental build feature
    "clean:tsbuildinfo": "rm -f tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo",

    // convenience script to run the clean steps in sequence
    "clean": "npm run clean:dist && npm run clean:tsbuildinfo",

    // discovers and runs test files
    "tests": "node --import tsx --test tests/**/*.test.ts",

    // executed before build; generates buildinfo.txt
    "prebuild": "npm run buildinfo",

    // convenience script to run the build steps in sequence
    "build": "npm run build:bundle && npm run build:tsc",

    // bundles the library into ESM and IIFE formats for distribution
    "build:bundle": "tsx scripts/build-bundle.ts",

    // compiles the library to declaration files and ESM JavaScript in dist/
    "build:tsc": "tsc --project tsconfig.build.json",

  },

  "devDependencies": {
    "@types/node": "latest",
    "esbuild": "latest",
    "tsx": "latest",
    "typescript": "^6.0.3"
  }

}

Script scripts/buildinfo.ts

Writes buildinfo.txt with the package version from package.json, appending the git short hash as semver build metadata (<version>+<hash>). If the version already contains +, the hash is appended as <version>+<existing>.<hash> instead. Falls back to the bare version when git is unavailable.

Script scripts/build-bundle.ts

Bundles the library to ESM and IIFE formats using esbuild. Entry points are resolved from the entrypoint condition in the exports field of the package.json. Note, the entrypoint condition is a custom, non-standard export condition used solely for build tooling.

Packages can be marked as external. Such packages must be available at runtime (e.g. in node_modules/ or on a CDN) and are not bundled into the output. To mark an import as external, add it to the Import Map scripts/import-map.json.

Files in the COPY_FILES array are copied to the output directory after esm bundling.

CSS processing

CSS processing can be enabled with the following addition to scripts/build-bundle.ts:

import { globSync } from 'node:fs';

// ...

console.log('[scripts/build-bundle.ts] CSS processing...');
const cssEntryPoints = globSync('src/**/*.css');
if (cssEntryPoints.length > 0) {
    try {
        await build({
            entryPoints: cssEntryPoints,
            outdir: 'dist/',
            outbase: 'src',
            entryNames: '[dir]/[name]',
            assetNames: '[dir]/[name]',
            platform: 'browser',
            bundle: true,
            minify: true,
            sourcemap: 'external',
            loader: {
                '.woff': 'file',
                '.woff2': 'file',
                '.ttf': 'file',
                '.eot': 'file',
                '.svg': 'file',
                '.png': 'file',
                '.jpg': 'file',
                '.jpeg': 'file',
                '.gif': 'file',
            },
            plugins: [esbuildLog],
        });
    } catch (error) {
        console.error('[scripts/build-bundle.ts] CSS processing failed:');
        for (const message of (error as BuildFailure).errors) {
            console.error(message);
        }
        process.exit(1);
    }
}
console.log('[scripts/build-bundle.ts] CSS processing finished.');

Then, add an exports entry in package.json for the CSS files:

{
  // ... ,
  "exports": {
    // ... ,
    "./*.css": "./dist/*.css"
  },
}

Consumers can then import the CSS files directly from the package:

import '@scope/package-name/path-in-dist/styles.css';

Import Map scripts/import-map.json

Marks imports as external and optionally rewrites them.

  • Falsy value: marks the import external without rewriting; the original specifier is preserved.
  • Truthy value: rewrites the import to the given specifier and marks it external.
{
  "rewrite-package": "https://cdn.jsdelivr.net/npm/rewrite-package@<VERSION>/dist/index.js",
  "rewrite-package-without-version": "https://cdn.jsdelivr.net/npm/rewrite-package/dist/index.js",
  "external-only-package": null
}

<VERSION> can be added in the given specifier. It is replaced with the version of the matching package from package.json.

tsconfig.build.json

tsconfig.json is intended for development only. tsconfig.build.json extends it with settings for compiling the source files for distribution.

See workspace template README and tsconfig.json for detailed explanations of all options.

{

  "extends": "./tsconfig.json",

  "compilerOptions": {

    // narrows root to src/ for distribution output
    "rootDir": "./src/",

    // nodenext enforces strict ESM compliance; imports must use explicit .js file extensions
    "module": "nodenext",
    "moduleResolution": "nodenext",

    // emit .d.ts declaration files
    "declaration": true,

    // emit .d.ts.map files mapping declarations back to source
    "declarationMap": true,

    // emit only .d.ts files, no JavaScript
    "emitDeclarationOnly": true,

    // output directory for emitted files
    "outDir": "./dist/"

  },

  // include only src/ files for distribution
  "include": [
    "src/**/*.ts"
  ],
  // exclude development files and directories from distribution
  "exclude": [
    "node_modules/",
    "dist/",
    "tests/",
    "scripts/"
  ]

}

package.json

{
  // ... ,
  "scripts": {
    // ... ,
    "build": "... && bun run build:tsc",
    "build:tsc": "tsc --project tsconfig.build.json"
  }
}

Exports condition import may point to the compiled output instead of the bundled output ./dist/index.bundle.js if "emitDeclarationOnly": false:

{
  // ... ,
  "exports": {
    ".": {
      // ... ,
      "import": "./dist/index.js"
    }
  },
  // ... ,
}

When "declaration": true, then exports condition types can be added to point to the declaration files:

{
  // ... ,
  "exports": {
    ".": {
      // ... ,
      "types": "./dist/index.d.ts",
      // ... ,
    }
  },
  // ... ,
}

Package Files Resolution

# placeholder:
  # <@SCOPE: <@SCOPE>
  # <PACKAGE_NAME: <PACKAGE_NAME>

The key question to ask is: does the package retain its import.meta.url identity at runtime? Everything else follows from it.

import.meta.url resolves to the URL of the containing file or bundle:

https://cdn.example.com/<@SCOPE>/<PACKAGE_NAME>/dist/index.bundle.js
file:///project/node_modules/<@SCOPE>/<PACKAGE_NAME>/dist/index.js
file:///project/scripts/dev.ts

When the package is NOT bundled into the consumer's output (is loaded as a discrete module at runtime), the package retains its import.meta.url identity: import.meta.url points to the containing file or bundle. URLs derived from import.meta.url resolve to the expected location regardless of how the package is distributed (transpiled only or bundled).

When the package is bundled into the consumer's output, it loses its import.meta.url identity: import.meta.url points to the consumer's bundle or file, not the original location of the package. URLs derived from import.meta.url silently resolve relative to the consumer's output.

The generated src/package-urls.ts scaffolds convenience URL exports based on import.meta.url. Its location in the package, the constructed URLs, and its exports entry in package.json are designed such that the exported URLs are at the same relative path to the package root regardless of the build strategy (bundled, transpiled, or imported). This has the following implications:

  1. If the package is used as a discrete module at runtime (e.g. loaded from node_modules/ or a CDN), then the URLs exported in src/package-urls.ts resolve to the expected location without requiring any special handling from the consumer. This is the recommended way to consume the generated package.
  2. The exports entry in package.json for ./package-urls can be marked external independently from the rest of the package, so that it retains its import.meta.url identity. See Import Map scripts/import-map.json.
  3. If the package is bundled into the consumer's output, then the consumer must consult the exports in src/package-urls.ts and ensure that the directories and files for the exported URLs exist in the final output (e.g. by copying them from node_modules/).

The exports entry in package.json for ./package-urls is not generated by default. The package author may add it to signal the consumers that the package resolves package URLs at ./package-urls and that they should be treated as a special case if the package is bundled.

  // ... ,
  "exports": {
    "./package-urls": {
      "entrypoint": "./src/package-urls.ts",
      "types": "./dist/package-urls.d.ts",
      "browser": "./dist/package-urls.bundle.js",
      "import": "./dist/package-urls.bundle.js"
    },
    // ...
  },
  // ...

Accessing package files at runtime

// src/package-urls.ts and dist/package-urls.js are at the same relative path to the package root
const packageUrl = new URL('../', import.meta.url);
const assetsUrl  = new URL('assets/<@SCOPE>/<PACKAGE_NAME>/', packageUrl);

// for `--target browser` and `--target node` (isomorphic)
export { };
const assetUrl = new URL('<ASSET>', assetsUrl);
const asset = await fetch(assetUrl).then(response => response.body);

// for `--target node` only
import { readFile } from 'node:fs/promises';
const assetUrl = new URL('<ASSET>', assetsUrl);
const asset = await readFile(assetUrl, 'utf-8');

⚠️ Beware that import.meta.url is replaced by document.currentScript.src during bundling in IIFE format since it is available only under ESM (research "import.meta.url vs document.currentScript.src").

"bin" field in package.json

For CLI packages, add the following to package.json:

{
  // ... ,
  "bin": "./dist/cli.bundle.js",
  "exports": {
    ".": {
      "entrypoint": "./src/cli.ts"
    }
  },
  // ...
}

src/cli.ts must begin with a hashbang so the OS knows which interpreter to invoke when the binary is executed directly.

#!/usr/bin/env node

DevOps

npm install
npm update

npm run clean
npm run build
npm run tests

npx tsx dist/cli.bundle.js -- example/

Change Management

  1. Create a new branch for the change.
  2. Make the changes and commit.
  3. Bump the version in package.json.
  4. Add an entry for the new version in CHANGELOG.md.
  5. Pull-request the branch.
  6. Ensure package artifacts are current.
  7. Publish.

Publish

~/.npmrc or .npmrc:

@temir.ra:registry=https://registry.npmjs.org/

~/.bunfig.toml or bunfig.toml:

[install.scopes]
"temir.ra" = { url = "https://registry.npmjs.org/" }
# registry.npmjs.org/
npm login
npm publish