Package Exports
- ts-node-pack
Readme
ts-node-pack
Pack a TypeScript package into a Node-compatible npm tarball — without modifying the source tree, without bundling, and without changing module resolution semantics.
Given a TypeScript package whose sources use .ts files and .ts in relative import specifiers, ts-node-pack produces a .tgz whose contents are plain .js + .d.ts with correct .js specifiers, ready to npm install into any Node ESM project.
Why
TypeScript 5.7 introduced rewriteRelativeImportExtensions, which lets you author import './foo.ts' and have tsc emit import './foo.js' in the compiled .js. But:
tscdoes not rewrite.tsspecifiers inside emitted.d.tsfiles.- Your
package.json(main,module,exports,types) still points at.ts. - You probably don't want the
.tssources in the published tarball at all.
ts-node-pack wraps tsc + npm pack and fills in exactly those gaps.
Install
npm install --save-dev ts-node-packRequires Node ≥ 20 and TypeScript ≥ 5.7 available in the package being packed (resolved via npx tsc).
Usage
ts-node-pack <packageDir> [--tsconfig <path>] [--emit-only] [--keep-temp] [--verbose]| Flag | Description |
|---|---|
--tsconfig <path> |
tsconfig to extend. Defaults to tsconfig.build.json if present, otherwise tsconfig.json. |
--emit-only |
Run emit + rewrites + validation, but skip npm pack. Prints the staging directory. |
--keep-temp |
Do not delete the temporary staging directory on exit. |
-v, --verbose |
Log each pipeline phase to stderr. |
-h, --help |
Show help. |
The resulting <name>-<version>.tgz is written to the current working directory.
Example
cd my-project
ts-node-pack ./packages/core --verbose
npm install ./my-core-1.2.3.tgzPipeline
- Resolve package — read
package.json, pick tsconfig. - Stage — create
mkdtemp()/package/. - Derived tsconfig — write
tsconfig.emit.jsoninside the temp dir thatextendsthe chosen tsconfig (by absolute path) and forcesoutDir,declaration,rewriteRelativeImportExtensions: true,noEmit: false. If the base tsconfig enablessourceMap,inlineSourceMap, ordeclarationMap,inlineSources: trueis also set so debuggers get full source-level fidelity without any.tsfiles in the tarball. - Emit — run
tsc -pagainst the derived config. - Rewrite
.d.ts— for each emitted.d.ts, rewrite./foo.ts→./foo.jsinimport/export from/ dynamicimport()specifiers. Non-relative specifiers are left alone. - Rewrite
package.json— rewrite.ts→.js(and →.d.tsundertypesconditions) inmain,module,types,typings,bin,exports, and thefilesarray. StripdevDependenciesandscripts. - Copy assets —
README*,LICENSE*,CHANGELOG*,NOTICE*. Source.tsfiles are never copied. - Validate — fail if any
.tsspecifier remains in emitted.js/.d.ts/package.json, or if a referenced entry point does not exist. - Pack —
npm packin the staging directory; move the tarball to the original CWD. - Cleanup — remove
.ts-node-pack/and the temp directory (unless--keep-temp).
The source tree is never mutated. All intermediate artifacts (derived tsconfig, staging dir, tarball) live under a single mkdtemp() directory that is removed on exit.
Sourcemaps
If your tsconfig has sourceMap (or inlineSourceMap / declarationMap) enabled, ts-node-pack automatically forces inlineSources: true so each emitted .map embeds its source text via sourcesContent. This gives full source-level debugging and "Go to Definition" without shipping .ts files — sidestepping dual-resolution hazards where a downstream bundler or TS project might pick ./foo.ts over ./foo.js. Detection is a shallow read of the chosen tsconfig; if you inherit sourceMap from a base config via extends, set it explicitly at the leaf.
Non-goals
- No bundling.
- No AST transforms.
- No custom module resolution.
- No
npm publishlogic.
License
MIT