Build and publish an npx command to npm with Typescript

Sandro Maglione

Sandro Maglione

Web development

npx allows to run a single executable file without installing a full npm package locally.

I recently implemented a static website generator that runs a single npx command to generate a full html website from markdown:

In this post we learn how to:

  • Create an npx executable script using Typescript
  • How to configure package.json to publish an npx command
  • How to bundle Typescript code to a single javascript executable file
  • How to publish the npx code to npm

Implement code to execute

The implementation is up to you 💁🏼‍♂️

The only requirement is adding a shebang in the first line of the executable code:

shebang: number sign and exclamation mark (#!) at the beginning of a script.

It tells the operating system (Unix) that this file is a node script.

bin.ts
#!/usr/bin/env node
 
import { Effect, Layer, LogLevel, Logger, ReadonlyArray, pipe } from "effect";
import * as Converter from "./Converter.js";
import * as Css from "./Css.js";
import * as FileSystem from "./FileSystem.js";
import * as LinkCheck from "./LinkCheck.js";
import { ChalkLogger } from "./Logger.js";
import * as SiteConfig from "./SiteConfig.js";
import * as Template from "./Template.js";

When running npx the code in this entry file will be executed (bin.ts/js in this example 👆).

In the code below the final line executes the full program as a Promise and logs unexpected errors (using Effect):

#!/usr/bin/env node
 
const program = Effect.gen(function* (_) {
  /// ...
});
 
const MainLive = Layer.mergeAll(
  /// ...
);
 
const runnable = program.pipe(
  Effect.provide(
    /// ...
  )
);
 
const main: Effect.Effect<never, never, void> = runnable.pipe(
  Effect.catchTags({
    /// ...
  })
);
 
Effect.runPromise(main).catch(console.error); 

package.json configuration (bin)

Inside package.json we define the npx command to run using bin.

bin can be either:

  • A single string with the name of the file to execute
  • An object where the key is the name of the command and the value is the file to execute

In the example below we define a menimal command that executes the code inside ./dist/bin.js.

This will create a npx menimal command on npm:

package.json
{
  "name": "menimal", 
  "version": "0.0.11", 
  "author": "Sandro Maglione",
  "description": "Generate a static html-only website from markdown and css",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/SandroMaglione/menimal.git"
  },
  "keywords": [
    "effect"
  ],
  "main": "./dist/bin.js", 
  "bin": { 
    "menimal": "./dist/bin.js"
  }, 
  "files": [ 
    "dist"
  ], 
  "scripts": {
    "tsc": "tsc -p tsconfig.json",
    "dev": "tsx src/bin.ts",
    "bundle": "tsup && tsx scripts/copy-templates.ts",
    "upload": "pnpm bundle && npm publish"
  },
  "dependencies": {
    "@effect/platform": "^0.43.7",
    "@effect/platform-node": "^0.42.7",
    "@effect/schema": "^0.61.5",
    "chalk": "^4.1.2",
    "effect": "^2.2.3",
    "gray-matter": "^4.0.3",
    "html-minifier": "^4.0.0",
    "lightningcss": "^1.23.0",
    "mustache": "^4.2.0",
    "node-html-parser": "^6.1.12",
    "showdown": "^2.1.0"
  },
  "devDependencies": {
    "@types/html-minifier": "^4.0.5",
    "@types/mustache": "^4.2.5",
    "@types/node": "^20.11.16",
    "@types/showdown": "^2.0.6",
    "tsup": "^8.0.1",
    "tsx": "^4.7.0",
    "typescript": "^5.3.3"
  }
}

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.

tsup: Bundle Typescript library

Since our script is written using Typescript, we need to bundle it to a single javascript file.

In this example I used tsup:

package.json
  "devDependencies": { 
    "@types/html-minifier": "^4.0.5",
    "@types/mustache": "^4.2.5",
    "@types/node": "^20.11.16",
    "@types/showdown": "^2.0.6",
    "tsup": "^8.0.1", 
    "tsx": "^4.7.0", 
    "typescript": "^5.3.3"
  }

Inside tsup.config.ts we define the bundling configuration:

  • Specify entry file (src/bin.ts)
  • Specify the output format (cjs for Node)
tsup.config.ts
import { defineConfig } from "tsup";
 
export default defineConfig({
  entry: ["src/bin.ts"],
  publicDir: false,
  clean: true,
  minify: true,
  format: ["cjs"], // 👈 Node
});

Now we can simply run the tsup command:

package.json
  "scripts": { 
    "tsc": "tsc -p tsconfig.json",
    "dev": "tsx src/bin.ts",
    "bundle": "tsup && tsx scripts/copy-templates.ts", 
    "upload": "pnpm bundle && npm publish"
  },

I added also a custom script copy-templates.ts that is run using tsx.

tsup will read the configuration from tsup.config.ts and tsconfig.json and bundle the full typescript code to a single bin.js filetsup will read the configuration from tsup.config.ts and tsconfig.json and bundle the full typescript code to a single bin.js file

This will generate a dist folder containing the bundled code (a single bin.js file):

All the code is bundled inside a single bin.js. Make sure to ignore this file using .gitignoreAll the code is bundled inside a single bin.js. Make sure to ignore this file using .gitignore

Publish library on npm

The final step is publishing the library on npm.

You need to have a valid account on npm to be allowed to publish a package ☝️

We define an upload command that bundles the code and executes npm publish:

You will be prompted to login to your npm account. You can also run the npm login command yourself.

package.json
  "scripts": { 
    "tsc": "tsc -p tsconfig.json",
    "dev": "tsx src/bin.ts",
    "bundle": "tsup && tsx scripts/copy-templates.ts",
    "upload": "pnpm bundle && npm publish"
  },

This is it!

You can now publish your own npx commands on npm. This comes handy when you want to execute some code without installing the package locally.

Check out the menimal package on npm.

If you are interested to learn more, every week I publish a new open source project and share notes and lessons learned in my newsletter. You can subscribe here below 👇

Thanks for reading.

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.