A complete walkthrough of publishing Cartlify — a React e-commerce UI kit — to npm for the first time.
The Milestone
Yesterday I published Cartlify to npm.
npm install cartlify
It sounds simple. But getting to that one line took more decisions, more configuration, and more trial and error than I expected.
This article covers everything — from setting up the build config to the actual publish command — so you don't have to figure it out the hard way.
What Is Cartlify?
Cartlify is a production-ready React + TypeScript + Tailwind CSS component library focused on e-commerce UI.
4 components that every e-commerce project needs:
- ProductCard — 3 layout variants, image gallery, wishlist, sale badges, skeleton loading
- CartDrawer — animated slide-in, focus trap, ESC dismiss, quantity stepper
- CheckoutStepper — horizontal/vertical, animated connectors, keyboard navigation
- PageLoader — 4 animation styles, 3 position modes
Plus 3 utility hooks, 11 tree-shakeable icons, 40+ CSS design tokens, full dark mode, and 141 Jest + React Testing Library tests.
Built so freelance developers and indie makers can skip the painful e-commerce UI layer and ship faster.
Why Publish to npm?
Before npm, Cartlify was only available on Gumroad as a paid download.
That's fine — but npm adds something Gumroad can't:
Developer sees Cartlify →
runs npm install cartlify →
evaluates the compiled output →
trusts the quality →
buys the full source on Gumroad
npm is a credibility and discovery channel — not just a distribution method. A package on npm signals that something is real, maintained, and production-ready.
Also: npmjs.com gets millions of developer searches every month. That's free traffic you can't get from Gumroad alone.
The Build Setup — tsup
The most important decision before publishing is how you bundle your library.
I chose tsup — a zero-config TypeScript bundler built on esbuild. Here's why:
| Tool | Config needed | Speed | Output |
|---|---|---|---|
| Rollup | Lots | Medium | ESM + CJS |
| Webpack | Heavy | Slow | CJS only |
| Vite lib mode | Some | Fast | ESM + CJS |
| tsup | Almost zero | Very fast | ESM + CJS + .d.ts |
My tsup.config.ts:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
sourcemap: false,
clean: true,
minify: true,
external: ['react', 'react-dom'],
esbuildOptions(options) {
options.alias = {
'@components': './src/components',
'@primitives': './src/primitives',
'@hooks': './src/hooks',
'@utils': './src/utils',
'@types': './src/types',
};
},
});
Key decisions:
-
format: ['cjs', 'esm']— supports both older and modern bundlers -
dts: true— generates.d.tsTypeScript type definitions -
minify: true— compiled output is minified (buyers get clean source on Gumroad) -
sourcemap: false— no source maps in public npm package -
external: ['react', 'react-dom']— don't bundle React itself
After running npm run build, the dist/ folder looks like:
dist/
├── index.js ← CommonJS
├── index.mjs ← ES Module
└── index.d.ts ← TypeScript types
The package.json Setup
This is the most critical file for npm publishing. Every field matters.
{
"name": "cartlify",
"version": "1.0.0",
"description": "Production-ready React e-commerce UI kit — ProductCard, CartDrawer, CheckoutStepper, PageLoader. Built with TypeScript, Tailwind CSS and Storybook.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsup src/index.ts",
"prepublishOnly": "npm run build && npm run lint && npm run test"
},
"keywords": [
"react",
"typescript",
"tailwind",
"ui-kit",
"ecommerce",
"cart",
"product-card",
"checkout",
"storybook",
"component-library",
"frontend"
],
"author": "Karthik G S <karthikgs.softengg@gmail.com>",
"license": "MIT",
"homepage": "https://cartlify.vercel.app",
"repository": {
"type": "git",
"url": "https://github.com/thirumalai77/cartlify"
},
"bugs": {
"url": "https://github.com/thirumalai77/cartlify/issues"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}
Key fields to get right:
files — only publish what buyers need:
"files": ["dist", "README.md"]
This stops src/, .storybook/, node_modules/, and test files from being included in the npm package.
exports — modern bundler resolution:
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
peerDependencies — don't bundle React:
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
prepublishOnly — runs before every publish automatically:
"prepublishOnly": "npm run build && npm run lint && npm run test"
This saved me from accidentally publishing broken code.
The .npmignore File
Create this in your root to stop unnecessary files from being published:
src/
.storybook/
*.stories.tsx
*.test.tsx
*.test.ts
node_modules/
.eslintrc.json
.prettierrc
tsconfig.json
tailwind.config.js
tsup.config.ts
coverage/
.github/
Without .npmignore, npm would publish your entire project including source code, test files, and config — making your package unnecessarily large.
Path Aliases — The Tricky Part
Cartlify uses path aliases to avoid ../../ imports:
import { Button } from '@primitives/Button';
import { cn } from '@utils/cn';
import type { Product } from '@types';
This works fine in development. But when you build for npm, the compiled output still has the alias references — and they break for the end user.
The fix is in tsup.config.ts:
esbuildOptions(options) {
options.alias = {
'@components': './src/components',
'@primitives': './src/primitives',
'@hooks': './src/hooks',
'@utils': './src/utils',
'@types': './src/types',
};
},
This tells tsup to resolve aliases during compilation — the output dist/ files have no aliases, just clean relative paths.
Creating the npm Account
- Go to npmjs.com → Sign Up
- Verify your email
- Enable 2FA — npm now requires this for publishing
- Login in terminal:
npm login
It opens a browser for 2FA confirmation. Once verified:
npm whoami
# → yourusername
The Dry Run — Always Do This First
Before publishing for real:
npm publish --dry-run
This shows exactly what will be uploaded without actually publishing:
npm notice 📦 cartlify@1.0.0
npm notice === Tarball Contents ===
npm notice 2.1kB README.md
npm notice 18.4kB dist/index.js
npm notice 16.2kB dist/index.mjs
npm notice 8.9kB dist/index.d.ts
npm notice === Tarball Details ===
npm notice name: cartlify
npm notice version: 1.0.0
npm notice filename: cartlify-1.0.0.tgz
npm notice package size: 12.3 kB
npm notice unpacked size: 45.6 kB
npm notice total files: 4
Check two things:
- Only
dist/andREADME.mdare listed ✅ - No source files, test files, or config files ✅
The Publish Command
npm publish --access public
Output:
npm notice Publishing to https://registry.npmjs.org/
+ cartlify@1.0.0
That's it. Live at npmjs.com/package/cartlify.
Adding npm Badges to README
After publishing, add these to your README.md:
[](https://npmjs.com/package/cartlify)
[](https://npmjs.com/package/cartlify)
[](https://opensource.org/licenses/MIT)
They render as clickable badges on GitHub — adds credibility instantly.
The Free npm + Paid Source Model
Cartlify uses a split model:
npm install cartlify (free)
→ Compiled dist/ output
→ ESM + CJS + TypeScript types
→ All 4 components usable
Gumroad ($29 one-time)
→ Full TypeScript source code
→ Storybook documentation
→ All hooks, icons, design tokens
→ README + usage examples
→ Future updates
The npm package gives developers everything they need to use Cartlify in a project. The Gumroad source lets them read, modify, and fully own the code.
This is the same model used by shadcn/ui, Tailwind UI, and most successful component libraries — free to use, paid to own fully.
What I Learned
1. tsup is the right tool for libraries.
Zero config, fast, outputs exactly what npm needs. Don't overthink the build setup.
2. prepublishOnly is your safety net.
It runs your build, lint, and tests automatically before every publish. You can't accidentally publish broken code.
3. Path aliases need special handling.
They work in development but break in compiled output without the esbuildOptions.alias config.
4. Always dry run first.
npm publish --dry-run saves you from publishing the wrong files. Run it every time.
5. files in package.json matters.
Without it, npm publishes your entire project. Specify exactly what should ship.
6. npm is a credibility signal.
A package on npm feels more legitimate than a Gumroad ZIP alone. Developers trust it more.
Try Cartlify
npm install cartlify
🔗 npm → npmjs.com/package/cartlify
🔗 GitHub → github.com/thirumalai77/cartlify
🔗 Live Storybook → cartlify.vercel.app
🛒 Full source on Gumroad ($29) → karthiksoftengg.gumroad.com/l/cartlify-react-ui-kit
What's Next
Now that Cartlify is on npm, next steps are:
- Product Hunt launch
- Grow npm downloads and Gumroad sales
- Write more tutorials around the components
Will post a 30-day update with real numbers — downloads, views, and sales.
If you've published an npm package before — what do you wish you'd known? Drop it in the comments.
Built by Karthik G S — Senior Frontend Engineer with 10+ years in React, TypeScript, and React Native. Building Cartlify as an indie product alongside a full-time role.
Tags: #react #typescript #npm #webdev #javascript #tutorial #showdev #indiehacker
Top comments (0)