Skip to content

Writing npm Packages ​

This guide covers how to create, develop, and publish npm packages for use in Meteor applications.

Creating a new npm package ​

To create a new npm package:

bash
mkdir my-package
cd my-package/
meteor npm init

The last command creates a package.json file and prompts you for the package information. You may skip everything but name, version, and entry point. You can use the default index.js for entry point. This file is where you set your package's exports:

js
// my-package/index.js
exports.myPackageLog = function() {
  console.log("logged from my-package");
};

Now apps that include this package can do:

js
import { myPackageLog } from 'my-package';

myPackageLog(); // > "logged from my-package"

When choosing a name for your npm package, be sure to follow the npm guidelines.

ES Modules and CommonJS ​

Modern npm packages typically use ES Modules syntax. Here's how to set up your package for both ESM and CommonJS:

ES Modules package ​

Create a package.json with "type": "module":

json
{
  "name": "my-package",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "exports": {
    ".": "./index.js"
  }
}

Then use ES Module syntax in your code:

js
// my-package/index.js
export function myPackageLog() {
  console.log("logged from my-package");
}

export async function fetchData() {
  // async operations work naturally
  const result = await someAsyncOperation();
  return result;
}

Dual ESM/CommonJS package ​

To support both module systems, you can provide separate entry points:

json
{
  "name": "my-package",
  "version": "1.0.0",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  }
}

Including in your app ​

When you are developing a new npm package for your app, there are a couple methods for including the package in your app:

Inside node_modules ​

Place the package in your app's node_modules/ directory, and add the package to source control. Do this when you want everything in a single repository:

bash
cd my-app/node_modules/
mkdir my-package
cd my-package/
meteor npm init
git add -f ./ # or use a git submodule

Place the package outside your app's directory in a separate repository and use npm link. Do this when you want to use the package in multiple apps:

bash
cd ~/
mkdir my-package
cd my-package/
meteor npm init

# Register the package globally
npm link

# Link it in your app
cd ~/my-app/
meteor npm link my-package

Other developers will also need to run the npm link command.

Using npm workspaces ​

For monorepo setups, npm workspaces provide a cleaner solution:

json
// root package.json
{
  "name": "my-monorepo",
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}

Then organize your packages:

my-monorepo/
├── package.json
├── packages/
│   └── my-package/
│       └── package.json
└── apps/
    └── my-meteor-app/
        └── package.json

After any method, edit the dependencies attribute of your app's package.json, adding "my-package": "1.0.0" (use the same version number you chose during meteor npm init).

Writing Meteor-compatible packages ​

When writing npm packages that will be used in Meteor, keep these considerations in mind:

Async/await patterns ​

Meteor 3 uses async/await throughout. Your package should follow the same patterns:

js
// Good - async pattern
export async function getUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

// Export both sync and async versions if needed
export function getUserDataSync(userId) {
  // sync implementation for client-side use
}

export async function getUserDataAsync(userId) {
  // async implementation
}

Environment detection ​

Use Meteor's environment detection when available:

js
export function doSomething() {
  if (typeof Meteor !== 'undefined') {
    // Running in Meteor
    if (Meteor.isServer) {
      // Server-side code
    } else if (Meteor.isClient) {
      // Client-side code
    }
  } else {
    // Running outside Meteor (tests, Node.js, etc.)
  }
}

Isomorphic packages ​

For packages that work on both client and server:

js
// utils.js - works everywhere
export function formatDate(date) {
  return new Intl.DateTimeFormat('en-US').format(date);
}

// server.js - server only
export async function queryDatabase(query) {
  // Database operations
}

// client.js - client only
export function showNotification(message) {
  // UI notifications
}

Then set up conditional exports:

json
{
  "name": "my-isomorphic-package",
  "exports": {
    ".": "./utils.js",
    "./server": "./server.js",
    "./client": "./client.js"
  }
}

Publishing your package ​

You can share your package with others by publishing it to the npm registry. While most packages are public, you can control who may view and use your package with private modules.

Public packages ​

To publish publicly:

  1. Create an npm account at npmjs.com
  2. Log in from the command line:
    bash
    npm login
  3. Publish your package:
    bash
    npm publish

When you're done, anyone can add your package to their app with npm install --save your-package.

Scoped packages ​

For organization packages, use scoped names:

bash
npm init --scope=@myorg
npm publish --access public

Users install with:

bash
npm install @myorg/my-package

Development workflow ​

If you want to share packages during development, we recommend using npm link or workspaces instead of the registry. If you use the registry, then every time you change the package, you need to:

  1. Increment the version number
  2. Publish
  3. Run npm update my-package inside your app

Overriding packages with a local version ​

If you need to modify a package to do something that the published version doesn't do, you can edit a local version of the package on your computer.

Let's say you want to modify an npm package. First, install it if you haven't already:

bash
meteor npm install --save some-package

Now the code has been downloaded to node_modules/some-package/. Add the directory to source control:

bash
git add -f node_modules/some-package/

Now you can edit the package, commit, and push, and your teammates will get your version of the package.

To ensure that your package doesn't get overwritten during an npm update, change the default caret version range in your package.json to an exact version.

Before:

json
"some-package": "^1.0.2",

After:

json
"some-package": "1.0.2",

Using a Git repository ​

An alternative method is maintaining a separate repository for the package and changing the package.json version to a git URL:

json
{
  "dependencies": {
    "some-package": "git+https://github.com/yourusername/some-package.git#main"
  }
}

Or use a specific commit:

json
{
  "dependencies": {
    "some-package": "git+https://github.com/yourusername/some-package.git#abc1234"
  }
}

Every time you edit the separate repo, you'll need to commit, push, and run npm update some-package.

TypeScript support ​

To add TypeScript support to your npm package:

Installing dependencies ​

bash
npm install --save-dev typescript

Configuration ​

Create a tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Package.json setup ​

json
{
  "name": "my-typescript-package",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}

Source code ​

typescript
// src/index.ts
export interface UserData {
  id: string;
  name: string;
  email: string;
}

export async function fetchUser(id: string): Promise<UserData> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

Testing your package ​

Set up testing for your npm package:

Jest configuration ​

bash
npm install --save-dev jest @types/jest ts-jest

Create jest.config.js:

js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/*.test.ts'],
};

Writing tests ​

typescript
// src/index.test.ts
import { formatDate } from './index';

describe('formatDate', () => {
  it('should format dates correctly', () => {
    const date = new Date('2024-01-15');
    expect(formatDate(date)).toMatch(/1\/15\/2024/);
  });
});

Package.json scripts ​

json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  }
}

Best practices ​

  1. Use semantic versioning - Follow semver for version numbers.
  2. Include a README - Document your package's API and usage.
  3. Add a LICENSE file - Specify the license for your package.
  4. Use .npmignore - Exclude development files from the published package.
  5. Test before publishing - Run your test suite before every publish.
  6. Use lockfiles - Commit package-lock.json for reproducible builds.
  7. Keep dependencies minimal - Only include necessary dependencies.
  8. Support tree-shaking - Use ES modules to enable tree-shaking in bundlers.