remorses/kimaki · Archived

npm-package

Opinionated TypeScript npm package template for ESM packages. Enforces src→dist builds with tsc, strict TypeScript defaults, explicit exports, and publish-safe package metadata. Relative source imports must use .ts or .tsx (never .js); tsc rewrites them via rewriteRelativeImportExtensions. Use this when creating or updating any npm package in this repo.

First seen May 12, 2026

Installation

$ npx skills add remorses/kimaki --skill npm-package

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from remorses/kimaki.

npx skills add remorses/kimaki

Browse all from remorses/kimaki

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1.1K
License LICENSE
Default branch main
Open issues 28
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

Version0.0.1

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 23,704 B
  • docs SUMMARY.md 376 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

<!-- Purpose: canonical checklist for TypeScript npm package layout and publish config. -->

npm-package

Use this skill when scaffolding or fixing npm packages.

Package.json rules

  1. Always set "type": "module".
  2. Always fill "description".
  3. Always include GitHub metadata:

- repository with type, url, and directory - homepage - bugs

  1. Always include meaningful keywords.
  2. Always export ./package.json.
  3. Exports structure must include:

- "." for runtime entrypoint (dist) - "./src" and "./src/*" pointing to .ts source files

  1. In every export object, put types first.

- For runtime exports (for example "."), point types to emitted declaration files in dist. - For source exports ("./src", "./src/"), point types to source files in src (not ./dist/.d.ts).

  1. Always include default in exports.
  2. files must include at least:

- src - dist - any runtime-required extra files (for example schema.prisma) - skills/ directory if the package ships an agent skill (see "Agent skill" section below). Skill files live at skills/<name>/SKILL.md, never at the package root. - if tests are inside src and gets included in dist, it's fine. don't try to exclude them - Do NOT create package-level README.md files. In workspaces, keep one README at the repository root. Package READMEs don't get read by anyone. The root README is the single source of truth for the whole project.

  1. scripts.build should be tsc && chmod +x dist/cli.js (skip the chmod if

the package has no bin). No bundling. Optionally include running scripts with tsx if needed to generate build artifacts.

Never put rm -rf, rimraf, or any delete in build. build runs on every edit, watch loop, and CI test job. Deleting dist/ there breaks live dev servers that hold it in their module graph (Bun fails with Failed to load bundled module '<pkg>/dist/index.js' until restarted) and throws away incremental compilation. A stale file only matters in a published tarball, which prepublishOnly already handles.

  1. prepublishOnly is the only place a delete belongs. It keeps a file

deleted from src/ out of the published package: ``json { "prepublishOnly": "rimraf dist \".tsbuildinfo\" && pnpm build" } ` Always use rimraf, never rm -rf. This holds for every package.json script, not just this one. rm -rf is not portable to Windows, and with no .tsbuildinfo present zsh aborts rm -rf dist .tsbuildinfo with no matches found, so the build after && never runs and you publish a stale dist/. sh and bash only survive because rm -f ignores the literal unmatched name. Install rimraf as a devDependency; never rely on npx/bunx` at publish time.

When the package also has a publish guard, chain it first so the guard still runs before anything is deleted: ``json { "prepublishOnly": "node -e \"if (!process.env.CI) process.exit(1)\" && rimraf dist \"*.tsbuildinfo\" && pnpm build" } ``

bin field

Use bin as a plain string pointing to the compiled entrypoint, not an object:

{ "bin": "dist/cli.js" }

The bin file must be executable and start with a shebang. After creating or building it, always run:

chmod +x dist/cli.js

Add the shebang as the first line of the source file (src/cli.ts):

#!/usr/bin/env node

tsc preserves the shebang in the emitted .js file. The chmod +x is already part of the build script, so prepublishOnly still gets it through pnpm build after the cleanup step.

Reading package version at runtime

When Node code needs the package version, prefer reading it from package.json via createRequire. This works cleanly in ESM packages without adding a JSON import assertion.

import { createRequire } from "node:module";

const require = createRequire(import.meta.url);
const packageJson = require("../package.json") as {
  version: string;
};

export const packageVersion = packageJson.version;
  • Use a relative path from the current file to package.json.
  • Read only the fields you need, usually version.
  • Prefer this over hardcoding the version or duplicating it in source files.

Resolving paths relative to the package

ESM does not have __dirname. Derive it from import.meta.url with the node:url and node:path modules, then resolve relative paths from there.

import url from "node:url";
import path from "node:path";

const __dirname = path.dirname(url.fileURLToPath(import.meta.url));

// e.g. from src/cli.ts → read SKILL.md under skills/<name>/SKILL.md
// (skill files always live in skills/<name>/SKILL.md, never at the package root)
const skillPath = path.resolve(__dirname, "../skills/mypkg/SKILL.md");

// from dist/cli.js (after tsc) → reach back to src/
const srcFile = path.resolve(__dirname, "../src/template.md");
  • Remember that tsc compiles src/ → dist/. At runtime the file lives in

dist/, so one .. gets you back to the package root.

  • From a file in src/ during dev (running with tsx), .. also reaches the

package root since src/ is one level deep.

  • Use path.resolve(__dirname, ...) instead of string concatenation so it

works on all platforms.

Detecting development mode

Check whether import.meta.url ends with .ts or .tsx. In dev you run source files directly (via tsx or bun), so the URL points to a .ts file. After tsc builds to dist/, the URL ends with .js.

const isDev =
  import.meta.url.endsWith(".ts") || import.meta.url.endsWith(".tsx");

This is useful for conditionally resolving paths that differ between src/ and dist/, or enabling dev-only logging without relying on NODE_ENV.

tsconfig rules

Use Node ESM-compatible compiler settings:

{
  "compilerOptions": {
    "allowImportingTsExtensions": true,
    "rewriteRelativeImportExtensions": true,
    "rootDir": "src",
    "outDir": "dist",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "ESNext",
    "lib": ["ESNext"],
    "declaration": true,
    "declarationMap": true,
    "noEmit": false,
    "strict": true,
    "noImplicitAny": false,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true,
    "useUnknownInCatchVariables": false
  },
  "include": ["src"]
}
  • Always use "rootDir": "src"
  • Add "DOM" to lib only when browser globals are needed.
  • Requires TypeScript 5.7+ (rewriteRelativeImportExtensions).
  • Install @types/node as a dev dependency whenever Node APIs are used.
  • If generation is required, keep generators in scripts/*.ts and invoke them

from package scripts before build/publish.

IMPORTANT! always use rootDir src. if there are other root level folders that should be type checked you should create other tsconfig.json files inside those folder. DO NOT add other folders inside src or the dist/ will contain dist/src, dist/other-folder. which breaks imports. the tsconfig.json inside these other folders can be minimal, using noEmit true, declaration false. Because usually these folders do not need to be emitted or compiled. just type checked. tests should still be put inside src. other folders can be things like scripts or fixtures.

Relative imports

Always write .ts or .tsx in relative source imports. Never write .js.

TypeScript 5.7+ rewrites those extensions to .js in dist/ when rewriteRelativeImportExtensions is on. Source then runs as-is in tsx, bun, Next.js, and Node type-stripping. Published JS still resolves.

import { helper } from "./utils.ts";
import { Button } from "./button.tsx";

tsc emits ./utils.js and ./button.js. Only relative paths (./, ../) are rewritten. Path aliases, # imports, and bare specifiers are not. npm packages should use relative imports anyway.

Docs: rewriteRelativeImportExtensions, TypeScript 5.7 path rewriting.

Preferred exports template

{
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    "./package.json": "./package.json",
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    },
    "./src": {
      "types": "./src/index.ts",
      "default": "./src/index.ts"
    },
    "./src/*": {
      "types": "./src/*.ts",
      "default": "./src/*.ts" // or .tsx for packages that export React components. if so all files should end with .tsx
    }
  }
}

Package.json imports map (internal # aliases)

Use imports when you need a package to swap between different implementations based on runtime (Node vs Bun vs browser vs SQLite vs better-sqlite3, etc.). Internal imports are #-prefixed, scoped to the package itself, and never leak to consumers. Consumers resolve through exports, not imports.

Point types at dist, not src

The TypeScript docs are explicit about this:

If the package.json is part of the local project, an additional remapping
step is performed in order to find the input TypeScript implementation
file... This remapping uses the outDir/declarationDir and rootDir
from the tsconfig.json, so using "imports" usually requires an explicit
rootDir to be set.

This variation allows package authors to write "imports" and "exports"
fields that reference only the compilation outputs that will be published
to npm, while still allowing local development to use the original
TypeScript source files.

In other words, TypeScript automatically walks from ./dist/foo.d.ts back to ./src/foo.ts using outDir → rootDir during compilation. You do not need to point types at src manually — let TypeScript remap it.

{
  "imports": {
    "#sqlite": {
      "bun": "./src/platform/bun/sqlite.ts",
      "node": {
        "types": "./dist/platform/node/sqlite.d.ts",
        "default": "./dist/platform/node/sqlite.js"
      },
      "default": {
        "types": "./dist/platform/node/sqlite.d.ts",
        "default": "./dist/platform/node/sqlite.js"
      }
    }
  }
}

Resolution flow when tsc sees import db from '#sqlite':

  1. imports["#sqlite"].node.types → ./dist/platform/node/sqlite.d.ts
  2. package.json is in the local project → apply the remap.
  3. Replace outDir (dist) with rootDir (src) → ./src/platform/node/sqlite.d.ts
  4. Replace .d.ts with the source extension .ts → ./src/platform/node/sqlite.ts
  5. Return ./src/platform/node/sqlite.ts (it exists on a fresh clone, no build needed).
  6. Otherwise fall back to ./dist/platform/node/sqlite.d.ts.

Why dist-first is correct

  • No chicken-and-egg. The remap is compile-time, so tsc works on a fresh

clone without dist/ existing yet.

  • Published map describes shipped files. Every imports entry points at

something that will actually be in the npm tarball. No stale src paths leaking into the published package.json.

  • Works under plain Node. If the package is loaded by Node without

TypeScript involvement, Node reads the same imports map at runtime and resolves to real dist/*.js files that exist.

  • Bun / browser runtime conditions can still point at src, because

those runtimes execute .ts directly and skip the build step.

Excluding heavy dependencies from server/client bundles

imports conditions can swap a heavy client-only dependency for a lightweight noop stub on the server (or vice versa). This keeps the SSR bundle small without changing application code.

{
  "imports": {
    "#prism": {
      "types": "./dist/prism.d.ts",
      "ssr": "./dist/prism-noop.js",
      "browser": "./dist/prism.js",
      "default": "./dist/prism.js"
    }
  }
}

src/prism.ts imports the real library (~500KB). src/prism-noop.ts exports the same interface with stub implementations that return input unchanged. Application code imports from #prism and gets the right version automatically.

Requirements

This only works when:

  • moduleResolution is node16, nodenext, or bundler
  • rootDir is set explicitly in tsconfig.json (the skill's tsconfig rules

already require "rootDir": "src")

  • outDir is set (already in the template)
  • resolvePackageJsonImports is not disabled (it is on by default for the

supported moduleResolution modes)

Anti-pattern: pointing types at src manually

{
  "imports": {
    "#sqlite": {
      "node": {
        "types": "./src/platform/node/sqlite.ts", // ❌ don't do this
        "default": "./dist/platform/node/sqlite.js"
      }
    }
  }
}

This works but:

  1. The published package.json advertises src/*.ts paths that may or may

not exist depending on what you include in files.

  1. It bypasses TypeScript's built-in remapping, which is the whole point of

the local-project imports feature.

  1. It is inconsistent with default — mixing source (for types) and dist

(for runtime) paths in the same entry is easy to get wrong.

Source of truth: TypeScript Modules Reference — package.json "imports" and self-name imports.

tests location

test files should be close with the associated source files. for example if you have an utils.ts file you will create utils.test.ts file next to it. with tests, importing from ./utils.ts. preferred testing framework is vitest (or bun if project already using bun test or depends on bun APIs, rare)

Agent skill

If the package ships an agent skill (SKILL.md for AI coding agents), place it at:

skills/<package-name>/SKILL.md

Never put SKILL.md at the package root. The skills/<name>/SKILL.md layout matches the convention used by the skills CLI so users can install it with:

npx -y skills add owner/repo

Add this installation snippet to the README so users know how to get the skill:

## Agent Skill

This package ships a skill file that teaches AI coding agents how and when to
use it. Install it with:

\`\`\`bash
npx -y skills add owner/repo
\`\`\`

Remember to add skills to the files array in package.json so the skill directory is included when publishing.

Keep the SKILL.md thin

The SKILL.md body should be a few lines, not a full docs dump. Put all real documentation in README.md (which already lives in files) and have the skill tell the agent to fetch it. This way agents always read the latest docs and the skill never goes stale.

The body stays thin, but the frontmatter description must be rich. It is what the agent sees in its main context, and it is the only signal the agent uses to decide whether to load the skill. Make it long enough to cover: what the package is, the core concepts and APIs, concrete trigger phrases the user might say, and explicit "ALWAYS load this skill when..." conditions. A one-sentence description is almost always too short. See the new-skill skill for full guidance on writing descriptions.

CLI package template:

---
name: mypkg
description: |
  mypkg is <what it does and the core concepts>. It exposes <main commands
  or APIs> and is used for <typical tasks>. ALWAYS load this skill when
  the user mentions mypkg, runs <binary>, edits files that import mypkg,
  or asks about <trigger keywords>. Load it before writing any code that
  touches mypkg so you know the correct usage patterns and gotchas.
---

# mypkg

Every time you use mypkg, you MUST run:

\`\`\`bash
mypkg --help # NEVER pipe to head/tail, read the full output
\`\`\`

Library package template:

---
name: mypkg
description: |
  mypkg is <what it does and the core concepts>. It exports <main APIs>
  and is used for <typical tasks>. ALWAYS load this skill when the user
  mentions mypkg, imports from mypkg, edits files that depend on it, or
  asks about <trigger keywords>. Load it before writing any code that
  touches mypkg so you know the correct usage patterns and gotchas.
---

# mypkg

Every time you work with mypkg, you MUST fetch the latest README:

\`\`\`bash
curl -s https://raw.githubusercontent.com/owner/repo/main/README.md # NEVER pipe to head/tail
\`\`\`

Because the SKILL.md body points at the README, the README must contain everything the agent needs: API reference, examples, gotchas, and rules. See the new-skill skill for the full pattern.

pnpm workspaces

When the project is a monorepo, use pnpm workspaces with flat ./* glob paths in pnpm-workspace.yaml. All packages live at the repo root as siblings, no nested packages/ directory:

packages:
  - ./*

This means the repo looks like:

my-monorepo/
  package.json        # root (private: true)
  pnpm-workspace.yaml
  cli/                # workspace package
  website/            # workspace package
  db/                 # workspace package
  errore/             # workspace package (submodule)

Common dev dependencies at root

Install shared dev tooling only at the root package.json so every workspace package uses the same version without duplicating installs:

{
  "private": true,
  "devDependencies": {
    "typescript": "^5.9.2",
    "tsx": "^4.20.5",
    "vitest": "^3.2.4",
    "oxfmt": "^0.24.0"
  }
}

Packages that need these tools (like tsc or vitest) will resolve them from the root node_modules via pnpm's hoisting. Do not add typescript, tsx, vitest, or oxfmt as devDependencies in individual workspace packages — only add them at root.

Package-specific dev dependencies (for example @types/node, rimraf, prisma) still go in each package's own devDependencies.

Cross-workspace dependencies

Use workspace:^ (not workspace:*) for local package versions so that when published, the dependency resolves to a caret range instead of a pinned version:

{
  "dependencies": {
    "my-utils": "workspace:^"
  }
}

Use pnpm install package@workspace:^ to add a workspace dependency, or add it to package.json manually with the workspace:^ protocol.

CI (GitHub Actions)

Standard CI workflow for pnpm workspace monorepos. Key points:

  • Checkout submodules with submodules: recursive if the repo uses git

submodules (common for shared libraries like errore).

  • Use pnpm/action-setup@v4 with the pnpm version matching your lockfile.
  • Use Node 24 (or latest LTS) via actions/setup-node@v4 with cache: pnpm.
  • Build workspace packages that export from dist/ before running tests,

since submodules and some packages have dist/ gitignored.

  • Run tests from the package directory, not root.

Example .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Tests
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install

      # Submodules and workspace packages with dist/ gitignored
      # need to be built after checkout before anything can import them.
      - name: Build workspace packages with dist/ exports
        run: |
          pnpm --filter my-lib run build
          pnpm --filter my-utils run build

      - name: Run tests
        run: pnpm test -- --run
        working-directory: cli

If the repo has Prisma schemas, add generate steps before tests:

- name: Generate Prisma client
  run: pnpm generate
  working-directory: cli

README

for the first section of readme use markup like this

<div align='center' class='hidden'>
    <br/>
    <br/>
    <h3>projectname</h3>
    <p>8-12 words description of the project. tagline.</p>
    <br/>
    <br/>
</div>

there cannot be markdown inside the html.

or a variant with a logo image:

<div align='center' class='hidden'>
    <br/>
    <br/>
    <img src='https://genql.dev/banner.png' width='380px'>
    <br/>
    <br/>
    <h3>Type safe Graphql query builder</h3>
    <h4>Write Graphql queries with type validation and auto completion</h4>
    <br/>
    <br/>
</div>

Notice the use of h3, not h1. and h4 for the tagline

class='hidden' is required to hide this if the README page is also imported in a holocron.so website. this way that html is only show in github readme and not holocron. which would display its own heading.

if the README is being imported in holocron also follow holocron best practices like use of Aside and Note to highlight content. Non known github jsx tags are simply ignored by github which will still look fine. If you want to show a callback both in github and holocron you can use the callback syntax from github which works in both places:

> [!NOTE]
> Useful information that users should know, even when skimming content.

.gitignore

For non-workspace (standalone) packages, always create a .gitignore with:

node_modules
dist
*.tsbuildinfo
.DS_Store

Workspace packages inside a monorepo inherit the root .gitignore, so this only applies to standalone packages.

Peer dependencies

Peer dependencies are installed by default by npm and pnpm. Use them to prevent a dependency from being duplicated in the consumer's node_modules tree. Common examples: react, react-dom, framework packages that must be singletons.

To make peer dependencies optional, add peerDependenciesMeta:

{
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0",
    "sass": "^1.70.0",
    "tsx": "^4.8.1"
  },
  "peerDependenciesMeta": {
    "sass": { "optional": true },
    "tsx": { "optional": true }
  }
}

react above is required (no meta entry), so consumers must install it. sass and tsx are optional; the package works without them but can use them if present.

common mistakes

  • never write .js in relative source imports. use .ts or .tsx. tsc rewrites them in dist/
  • if you need to use zod always use latest version
  • always install packages as dev dependencies if used only for scripts, testing or types only
  • never use rm -rf in a package.json script. always use rimraf, installed as a dev dependency. rm -rf is not portable to Windows and zsh aborts the whole && chain when a glob matches nothing
  • never delete anything in build. deletes belong in prepublishOnly only
  • never use em-dashes (—) or dashes as inline separators (like word - word) in README or documentation. instead restructure the sentence: use periods to split into two sentences, colons, commas, or parentheses
  • never add badge images (shields.io, etc.) or any images you don't have locally in the repo. don't invent image URLs