Vendoring #

Vendoring is the practice of including third-party dependency code directly inside the project repository — instead of relying on a package manager to download it at build time. This concept is older than npm itself, rooted in the Go and C/C++ ecosystems where there was no centralized package manager. In the TypeScript and Node.js ecosystem, full vendoring is rarely done because npm/yarn/pnpm are very mature, but understanding when and how to do it remains relevant — especially for airgapped environments (no internet access), deep customization needs on third-party libraries, or mitigating supply chain attack risks. This article covers vendoring comprehensively: its definition, when it’s appropriate, how to implement it correctly, and the modern alternatives that are often better.

What Is Vendoring? #

In the Node.js/TypeScript ecosystem, “vendor” refers to code that isn’t your own but is included directly in the repository:

Without vendoring (the common way):
  package.json → dependencies are listed
  npm install  → npm downloads from the registry into node_modules/
  node_modules/ → in .gitignore, not committed

With vendoring:
  library code → copied into vendor/ or node_modules/ in the repository
  node_modules/ → COMMITTED to git (or just certain parts)
  npm install   → not needed or only for non-vendor dependencies

Several concepts that are often mixed up should be distinguished:

ConceptDescription
Full vendoringThe entire node_modules/ is committed to git
Selective vendoringOnly certain libraries are copied into vendor/
Lockfile pinningpackage-lock.json/yarn.lock committed, versions locked but not vendored
Private registryLibraries mirrored to an internal registry (Artifactory, Verdaccio)

When Vendoring Is Appropriate #

Vendoring is a trade-off with real costs (larger repo size, extra maintenance). Use it only when the benefits clearly outweigh the costs:

Scenarios Where Vendoring Is Right #

✓ Airgapped environments (no internet access at build/deploy time)
  → Production servers on an isolated network that can't reach the npm registry

✓ Deep customization of a third-party library
  → Need to modify the library source and don't want to fork + publish it yourself

✓ Abandoned libraries that are still needed
  → No more releases, but the functionality is still critical

✓ Extreme reproducible-build requirements
  → The npm registry can go down, packages can be unpublished (remember the 2016 left-pad incident)

✓ Strict security auditing of executed code
  → Need manual review of every line of code entering the system

Scenarios Where Vendoring Is Wrong #

✗ "To make builds faster" → Use the npm cache or Turbopack
✗ "To be more stable" → Use a lockfile and dependency pinning
✗ "Afraid a library will disappear from npm" → Use a private registry mirror
✗ Libraries that update frequently → Very high maintenance overhead
✗ Large libraries with many sub-dependencies → The repo will bloat enormously

Vendor Directory Structure #

If you decide to vendor a particular library, use a consistent, easy-to-understand structure:

my-project/
├── src/                        # Project source code
│   ├── index.ts
│   └── services/
├── vendor/                     # Vendored libraries
│   ├── README.md               # IMPORTANT: document why each lib is vendored
│   ├── some-utils/             # First library
│   │   ├── index.ts
│   │   ├── package.json        # Keep the original package.json for reference
│   │   ├── LICENSE             # REQUIRED: keep the license
│   │   └── VENDOR_INFO.md      # Version, date, reason, and modifications made
│   └── another-lib/
│       ├── index.js
│       ├── index.d.ts          # Type declaration if the library is JavaScript
│       └── VENDOR_INFO.md
├── tsconfig.json
└── package.json

VENDOR_INFO.md — Required Documentation for Every Vendored Library #

Every vendored library must have a documentation file explaining its context:

# VENDOR INFO: some-utils

## Metadata
- **Original version:** 2.3.1
- **Source:** https://github.com/contoh/some-utils
- **Vendored on:** 2025-05-07
- **Vendored by:** Budi Santoso (@budi)

## Reason for Vendoring
This library was vendored because our production servers are on an airgapped
network and can't reach the npm registry during deployment.

## Modifications Made
- Line 47 in `utils.ts`: Bug fix #1234 — the parseDate function didn't handle
  negative timezone offsets correctly. A PR has been submitted upstream:
  https://github.com/contoh/some-utils/pull/567

## Review Schedule
Next security review: 2025-08-07 (every 3 months)

## License
MIT License — see the LICENSE file

TypeScript Configuration for Vendor #

For TypeScript to recognize modules in the vendor/ directory, configure tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "some-utils": ["vendor/some-utils/index.ts"],
      "some-utils/*": ["vendor/some-utils/*"],
      "another-lib": ["vendor/another-lib/index.d.ts"]
    }
  },
  "include": [
    "src/**/*",
    "vendor/**/*.ts",
    "vendor/**/*.d.ts"
  ]
}

With this configuration, imports in the source code look identical to regular library imports:

// src/services/utils.ts
import { parseDate, formatDuration } from "some-utils"; // ✓ Resolves to vendor/some-utils/

// TypeScript has full type knowledge because there's .ts source in vendor
const tanggal = parseDate("2025-05-07");

Writing Type Declarations for JavaScript Libraries #

Many old vendored libraries are only available in JavaScript without type declarations. You need to write a .d.ts file yourself:

Simple Declarations #

// vendor/legacy-lib/index.d.ts

// Option 1: declare module — for libraries imported by name
declare module "legacy-lib" {
  // Export functions with the types you know
  export function hitungHarga(
    harga: number,
    kuantitas: number,
    diskon?: number
  ): number;

  export function formatMata(
    jumlah: number,
    matauang?: string
  ): string;

  // Export interfaces for data types
  export interface KonfigurasiHarga {
    ppn: number;
    maksDiskon: number;
    matauangDefault: string;
  }

  export function inisialisasi(konfig: KonfigurasiHarga): void;

  // Default export if the library uses module.exports = ...
  export default {
    hitungHarga,
    formatMata,
    inisialisasi,
  };
}

Declarations for Libraries Using Global Variables #

Some old libraries inject global variables (for example, libraries loaded via a <script> tag):

// vendor/legacy-analytics/index.d.ts

// Augment the global Window object
declare global {
  interface Window {
    LegacyAnalytics: {
      track(event: string, properties?: Record<string, unknown>): void;
      identify(userId: string, traits?: Record<string, unknown>): void;
      page(name?: string): void;
    };
  }

  // Or as a direct global variable
  const LegacyAnalytics: Window["LegacyAnalytics"];
}

export {}; // Makes this file a module

Gradual Declarations — Start from any #

If the library is very large and you don’t have time to write the whole declaration, start from any and improve it gradually:

// vendor/massive-library/index.d.ts
// TODO: Add more specific types gradually
// Issue: https://github.com/organisasi/proyek/issues/123

declare module "massive-library" {
  // Start with any — will be improved gradually
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const lib: any;
  export = lib;
}

Vendoring Alternatives That Are Often Better #

Before deciding to vendor, consider these alternatives that are generally easier to manage:

1. Lockfile Pinning (The Most Common Way) #

Commit package-lock.json or yarn.lock to git. This ensures all developers and CI/CD use exactly the same versions:

# npm — package-lock.json is generated automatically
npm install

# Consistent installation using the lockfile (for CI/CD)
npm ci    # Faster than npm install, strictly uses the lockfile

# yarn — yarn.lock is generated automatically
yarn install
yarn install --frozen-lockfile  # Fails if the lockfile needs updating (for CI/CD)

# pnpm — pnpm-lock.yaml is generated automatically
pnpm install
pnpm install --frozen-lockfile

2. npm pack — Vendoring One Package as a Tarball #

A cleaner alternative to copying source code — download the package as a .tgz file and commit it to the repo:

# Download the package as a tarball without installing it
npm pack [email protected]
# Produces: some-utils-2.3.1.tgz

# Move it to the vendor directory
mkdir -p vendor
mv some-utils-2.3.1.tgz vendor/
// package.json — reference the local tarball as a dependency
{
  "dependencies": {
    "some-utils": "file:./vendor/some-utils-2.3.1.tgz"
  }
}
# Install as usual — npm will use the local tarball
npm install

The advantages of this approach: library code isn’t scattered in a vendor directory, stays in a format npm knows, and is easy to update by replacing the .tgz file.

3. Private Registry — For Larger Teams #

For organizations that need to mirror the entire npm catalog or publish internal packages:

# Verdaccio — a lightweight, self-hosted private npm registry
npm install -g verdaccio
verdaccio

# Configure npm to use the private registry
npm set registry http://localhost:4873

# Publish an internal package to the private registry
npm publish --registry http://localhost:4873
// .npmrc — per-scope registry configuration
@organisasi:registry=https://npm.organisasi.com
//npm.organisasi.com/:_authToken=${NPM_TOKEN}

4. patch-package — Patching Without Full Vendoring #

If you only need to modify one or two lines in a third-party library, patch-package is a far better solution than full vendoring:

# Install patch-package
npm install --save-dev patch-package

# Edit the file in node_modules (only to create a patch, not permanent)
# For example: node_modules/some-utils/utils.js

# Create a patch file from the changes
npx patch-package some-utils

# The patch is saved at patches/some-utils+2.3.1.patch
# This file is committed to git

# The patch is applied automatically after npm install via a postinstall script
// package.json
{
  "scripts": {
    "postinstall": "patch-package"
  },
  "devDependencies": {
    "patch-package": "^8.0.0"
  }
}

Security Risks and Supply Chain Attacks #

Vendoring is often motivated by security, but it’s important to understand which threat types it handles and which it doesn’t:

flowchart TD
    A[Supply Chain Threats] --> B[Typosquatting]
    A --> C[Dependency Confusion]
    A --> D[Malicious Publish]
    A --> E[Compromised Maintainer]

    B --> B1[Package names similar to popular ones\ne.g.: 'lodahs' instead of 'lodash']
    C --> C1[Internal packages whose name\nalso exists on public npm]
    D --> D1[Packages that were initially good\nthen changed to become malicious]
    E --> E1[Maintainer account\nhacked, code changed]

    B1 --> F[Mitigation: Lockfile + audit]
    C1 --> G[Mitigation: Package scopes + private registry]
    D1 --> H[Mitigation: Lockfile + SRI hashes]
    E1 --> I[Mitigation: Vendoring + manual review]

    style F fill:#51cf66,color:#fff
    style G fill:#51cf66,color:#fff
    style H fill:#339af0,color:#fff
    style I fill:#fcc419,color:#000

Dependency Security Practices #

# Audit known vulnerabilities
npm audit
npm audit fix          # Fix automatically
npm audit fix --force  # Including breaking changes (use with care)

# Check the licenses of all dependencies
npx license-checker --summary

# Check for outdated packages
npm outdated

# Verify package integrity after installation
# package-lock.json contains an integrity hash (SHA-512) for every package

Dependency Management Strategy Comparison #

Strategy            Advantages                    Disadvantages
─────────────────────────────────────────────────────────────────────
Lockfile only       Easy, industry standard       Depends on the npm registry
Selective vendoring Full control over critical libs High maintenance overhead
npm pack (tarball)  Cleaner than copying source    Still needs manual updates
patch-package       Small changes without vendoring Depends on patch-package
Private registry    Scalable for large teams       Additional infrastructure
Full vendoring      Independent of the internet    Very large repository

Summary #

  • Vendoring is rarely needed in the modern Node.js/TypeScript ecosystem — a lockfile (package-lock.json, yarn.lock) is enough for most reproducible-build needs.
  • Vendor only if there’s a strong reason — airgapped environments, deep customization of a library that can’t be forked, or abandoned libraries that are still critical.
  • Always document vendored code — create a VENDOR_INFO.md for every vendored library; explain the version, date, reason, and modifications made.
  • Keep the original LICENSE file — vendoring doesn’t change the license; violating a library’s license is a legal risk.
  • Write type declarations (.d.ts) for vendored JavaScript libraries so TypeScript stays type-safe; start from any if the library is very large and improve gradually.
  • npm pack is cleaner than copying source code — the library is stored as a .tgz tarball and npm install still works normally.
  • patch-package is the best solution for small modifications to third-party libraries without needing full vendoring — patches are stored as diffs and applied automatically after npm install.
  • Private registries (Verdaccio, Artifactory) are a better solution than vendoring for large teams that need dependency control without bloating the repo size.
  • Audit dependencies regularly with npm audit — lockfiles and vendoring don’t protect against vulnerabilities discovered after a library is installed/vendored.

← Previous: TS Config   Next: Multi Threading →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact