No description
  • TypeScript 89.9%
  • JavaScript 10.1%
Find a file
2026-05-19 19:17:34 +02:00
scripts Updated from @temir.ra/template@0.1.3 template. 2026-04-13 16:29:31 +02:00
src Updated from @temir.ra/template@0.1.3 template. 2026-04-13 16:29:31 +02:00
template 2. Cleaned up minor README.md inconsistencies. 2026-05-19 18:57:44 +02:00
tests 3. Aligned wording with [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119). 2026-05-19 18:57:15 +02:00
.gitignore 5. Updated from @temir.ra/template@0.1.5 template. 2026-04-13 23:35:38 +02:00
CHANGELOG.md version 0.7.6 2026-05-19 19:16:58 +02:00
LICENSE carryover from dev as 0.1.0 (#1) 2026-02-27 20:14:40 +00:00
package.json version 0.7.6 2026-05-19 19:16:58 +02:00
README.md 2. Cleaned up minor README.md inconsistencies. 2026-05-19 18:57:44 +02:00
tsconfig.json 4. Updated files from template template. 2026-05-19 19:11:22 +02:00

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/build-bundle.ts
      1. CDN Map scripts/cdn-rewrite-map.json
    3. tsconfig.build.json
    4. Asset Resolution
      1. Externalized - loaded from CDN or node_modules/
      2. Bundled - absorbed into the consumer's output
      3. Contract
        1. Scoped assets directory convention
        2. Accessing assets
        3. README statement for library consumers
        4. When the library is bundled by the consumer
    5. "bin" field in package.json
  3. DevOps
    1. Change Management
    2. Publish

Quick Start

# placeholder:
    # <TEMPLATE_PACKAGE: @temir.ra/create-ts-lib
    # <TEMPLATE_NAME: @temir.ra/ts-lib
    # <NEW_PACKAGE: <NEW_PACKAGE>
      # is used as:
      #   - the path where the package is created
      #   - the "name" field in the generated package.json
    # <@_VERSION: <@_VERSION>

# pinned version
bun info "@temir.ra/create-ts-lib" version
bun create --no-install --no-git "@temir.ra/ts-lib<@_VERSION>" <NEW_PACKAGE>

# latest
# clear the cache to pick up the latest version
bun pm cache rm
bun create --no-install --no-git "@temir.ra/ts-lib" <NEW_PACKAGE>

# templates only copy files, run install and any setup scripts manually
cd <NEW_PACKAGE>
bun install

Documentation

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

The central addition over create-workspace is a build pipeline for distributing the library. Two build strategies are supported:

  • Bundling (scripts/build-bundle.ts) - generated by default; bundles the library to ESM and IIFE formats.
  • TSC compilation (tsconfig.build.json + build:tsc) - optional, not generated by default; compiles source files one-for-one to ESM JavaScript and declaration files.

Both strategies can be combined.

package.json

Selected fields are documented in the create-workspace README.

See npmjs documentation on package.json for detailed explanations of all fields.

The following lists the additions and overrides relative to create-workspace.

The generated package is pre-configured with build:bundle only. See TSC Compilation and "bin" field for extending it.

{

  // ... ,

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

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

  // 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
  // NOT for use in source files compiled by tsconfig.build.json
  // the .js extension is required in import statements (nodenext compliance)
  "imports": {
    "#src/*.js": "./src/*.ts"
  },

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

  // files to include in the published package
  "files": [
    "scripts/buildinfo.ts",
    "scripts/build-bundle.ts",
    "scripts/cdn-rewrite-map.json",
    "CHANGELOG.md",
    "buildinfo.txt",
    "dist/",
    "tests/"
  ],

  "scripts": {

    // ... ,

    // 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 *.tsbuildinfo || true",

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

    // discovers and runs test files
    "tests": "bun test",

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

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

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

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

    // bundles the CLI into a single file for distribution; requires the "bin" field to be set to the bundled output
    "build:cli-bundle": "bun build src/cli.ts --entry-naming \"[dir]/[name].bundle.[ext]\" --outdir dist --target node --format esm --minify --sourcemap=external"

  }

}

It is highly recommended to include tests/ in the files field. Note, that tests/ is not included by default in the generated package.json.

Script scripts/build-bundle.ts

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

Import specifiers can be rewritten to CDN URLs via scripts/cdn-rewrite-map.json.

CDN Map scripts/cdn-rewrite-map.json

Maps import specifiers to CDN URLs. During bundling, matching specifiers in source files are rewritten to their CDN equivalents.

<VERSION> in a URL is replaced with the version of the matching package from package.json.

{
  "import-specifier": "https://cdn.jsdelivr.net/npm/package-name@<VERSION>/dist/index.bundle.js"
}

tsconfig.build.json

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

See create-workspace 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:

{
  // ... ,
  "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",
      // ... ,
    }
  },
  // ... ,
}

Asset Resolution

The key question to ask is: does my library retain its own URL at runtime? Everything else follows from it.

# placeholder:
  # <@SCOPE: <@SCOPE>
  # <LIB_NAME: <LIB_NAME>
  # <VERSION: <VERSION>

Externalized - loaded from CDN or node_modules/

The consumer does not bundle the library. It is loaded as a discrete module at runtime. Module identity is preserved regardless of the URL form:

https://cdn.jsdelivr.net/npm/<@SCOPE>/<LIB_NAME>@<VERSION>/dist/index.js
file:///project/node_modules/<@SCOPE>/<LIB_NAME>/dist/index.js
https://your-own-cdn.com/<LIB_NAME>/index.js

import.meta.url is reliable in all these cases. fetch() is the correct I/O mechanism because it accepts both https:// and file:// URLs, making it universally correct for isomorphic (--target node or --target browser) libraries.

Bundled - absorbed into the consumer's output

The consumer's bundler absorbs the library into their own output. Module identity is destroyed - import.meta.url now points to the consumer's bundle, not the library's. However, the path relationship can be preserved by convention: if the consumer copies the library's assets into their build output at the same relative path the library expects, import.meta.url resolution continues to work correctly.

Contract

If the library has runtime assets, the following contract applies:

The library must:

  1. Place assets in a scoped assets directory - see Scoped assets directory convention below.
  2. Access assets via import.meta.url - see Accessing assets below.
  3. Document the bundled case - see README statement for library consumers below.

The consumer must,

  • when loading the library externalized: do nothing - the library's import.meta.url works as-is, and assets load from their original location (CDN or node_modules/).
  • when bundling the library:
    1. Copy the assets - see When the library is bundled by the consumer below.
    2. Ensure the assets are served
      • if the consumer is a web server, ensure the copied assets are served as static files
      • if the consumer is a bundler, ensure the copied assets are included in the bundle output

Scoped assets directory convention

Place all runtime assets under a scoped directory that mirrors the package name:

assets/
└── <@SCOPE>/
    └── <LIB_NAME>/
        └── ...

This also prevents naming collisions when multiple libraries are bundled into the same consumer app. Each library's assets live under their own namespace; no library can shadow another's files.

Add assets/ to the files field in package.json so that the assets are included in the published package:

  "files": [
    "dist",
    "CHANGELOG.md",
    "buildinfo.txt",
    "assets"
  ],

Accessing assets

Construct asset URLs directly from import.meta.url.

const packageUrl = new URL('../', import.meta.url);
const assetsUrl  = new URL('assets/<@SCOPE>/<LIB_NAME>/', packageUrl);


// for --target browser
const asset = await fetch(new URL('<ASSET>', assetsUrl)).then(r => r.json());

// for --target node
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
const asset = JSON.parse(await readFile(fileURLToPath(new URL('<ASSET>', assetsUrl)), 'utf-8'));

README statement for library consumers

## Asset resolution

This library resolves assets at runtime using `import.meta.url`. If you bundle this library into your application, copy `node_modules/<@SCOPE>/<LIB_NAME>/assets/<@SCOPE>/<LIB_NAME>/` into your build output directory alongside your bundle.

When the library is bundled by the consumer

The consumer must copy the assets into their build output at the same relative path the library expects:

consumer output/
├── bundle.js               ← import.meta.url points here
└── assets/
    └── <@SCOPE>/
        └── <LIB_NAME>/
            └── ...         ← copied; relative path preserved

From the bundle's perspective assets/<@SCOPE>/<LIB_NAME>/ is at the same directory level as bundle.js, so new URL('assets/<@SCOPE>/<LIB_NAME>/...', import.meta.url) resolves correctly. No code configuration is needed - only the file copy.

"bin" field in package.json

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

{
  // ... ,
  "bin": "./dist/cli.bundle.js",
  "scripts": {
    // ... ,
    "build": "... && bun run build:cli-bundle",
    "build:cli-bundle": "bun build src/cli.ts --entry-naming \"[dir]/[name].bundle.[ext]\" --outdir dist --target node --format esm --minify --sourcemap=external"
  }
}

src/cli.ts must begin with a hashbang so the OS knows which interpreter to invoke when the binary is executed directly. Bun preserves it in the bundled output.

#!/usr/bin/env node

If the package exports a CLI only and is not intended to be imported in other packages, the exports field can be omitted.

DevOps

bun update
bun install

bun run clean
bun run build
bun run tests

# see publish section for publish instructions

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

Publish to the public npm registry.

# authenticate
npm login
# publish
bun publish --registry https://registry.npmjs.org/ --access public