Extending
Adding Package Managers

Adding a New Package Manager

Comprehensive guide to integrating additional package managers (e.g., pnpm, Bun, Yarn Berry) into React Kickstart so they can be detected, selected, and used throughout the project generation process.

Current Support: React Kickstart currently supports npm and Yarn Classic. This guide shows how to add pnpm, Bun, and other package managers.


๐ŸŽฏ Integration Overview


๐Ÿ“‹ Complete Integration Guide

Add Detection and Metadata

File: src/utils/process/package-managers.js

Extend the package manager detection system:

// Add pnpm to the managers map
const managers = {
  npm: { available: false, version: null, recommended: false, error: null },
  yarn: { available: false, version: null, recommended: false, error: null },
  pnpm: { available: false, version: null, recommended: false, error: null },
}
 
// Add pnpm detection function
async function detectPnpm(managers, verbose) {
  try {
    const { stdout } = await execa('pnpm', ['--version'])
    managers.pnpm.available = true
    managers.pnpm.version = stdout.trim()
    
    // Check if pnpm is recommended (has workspace or monorepo indicators)
    try {
      const hasWorkspace = await CORE_UTILS.fileExists('pnpm-workspace.yaml')
      const hasLockfile = await CORE_UTILS.fileExists('pnpm-lock.yaml')
      managers.pnpm.recommended = hasWorkspace || hasLockfile
    } catch (error) {
      // Ignore workspace detection errors
    }
    
    if (verbose) {
      UI_UTILS.log(`โœ“ pnpm ${managers.pnpm.version} detected`)
    }
  } catch (error) {
    managers.pnpm.error = error.message
    if (verbose) {
      UI_UTILS.warn(`โœ— pnpm not available: ${error.message}`)
    }
  }
}
 
// Add pnpm command mappings
const PACKAGE_MANAGER_COMMANDS = {
  npm: {
    install: 'npm install',
    installDev: 'npm install --save-dev',
    installProd: 'npm install --save',
    run: 'npm run',
    create: 'npm create',
    exec: 'npx'
  },
  yarn: {
    install: 'yarn install',
    installDev: 'yarn add --dev',
    installProd: 'yarn add',
    run: 'yarn',
    create: 'yarn create',
    exec: 'yarn dlx'
  },
  pnpm: {
    install: 'pnpm install',
    installDev: 'pnpm add --save-dev',
    installProd: 'pnpm add --save',
    run: 'pnpm',
    create: 'pnpm create',
    exec: 'pnpm dlx'
  }
}
 
// Add pnpm-specific features
const PACKAGE_MANAGER_FEATURES = {
  npm: {
    lockfile: 'package-lock.json',
    nodeModules: 'node_modules',
    workspaces: true,
    speed: 'medium',
    diskUsage: 'high'
  },
  yarn: {
    lockfile: 'yarn.lock',
    nodeModules: 'node_modules',
    workspaces: true,
    speed: 'fast',
    diskUsage: 'high'
  },
  pnpm: {
    lockfile: 'pnpm-lock.yaml',
    nodeModules: 'node_modules',
    workspaces: true,
    speed: 'fastest',
    diskUsage: 'low',
    hoisting: false,
    strictPeerDeps: true
  }
}

Update Detection Function

File: src/utils/process/package-managers.js

Update the main detection function to include new package managers:

export async function detectPackageManagers(verbose = false) {
  const managers = {
    npm: { available: false, version: null, recommended: false, error: null },
    yarn: { available: false, version: null, recommended: false, error: null },
    pnpm: { available: false, version: null, recommended: false, error: null },
    bun: { available: false, version: null, recommended: false, error: null },
  };
 
  // Run all detections in parallel for better performance
  await Promise.all([
    detectNpm(managers, verbose),
    detectYarn(managers, verbose),
    detectPnpm(managers, verbose),
    detectBun(managers, verbose),
  ]);
 
  // Determine the best package manager
  const available = Object.entries(managers)
    .filter(([_, manager]) => manager.available)
    .map(([name, manager]) => ({ name, ...manager }));
 
  // Priority order: recommended > fastest > most compatible
  const recommended =
    available.find((manager) => manager.recommended) ||
    available.find((manager) => manager.name === "pnpm") ||
    available.find((manager) => manager.name === "bun") ||
    available.find((manager) => manager.name === "yarn") ||
    available.find((manager) => manager.name === "npm");
 
  return {
    managers,
    available,
    recommended: recommended?.name || "npm",
  };
}
 
// Add package manager comparison utility
export function comparePackageManagers(managers) {
  return Object.entries(managers)
    .filter(([_, manager]) => manager.available)
    .map(([name, manager]) => {
      const features = PACKAGE_MANAGER_FEATURES[name];
      return {
        name,
        version: manager.version,
        recommended: manager.recommended,
        features: {
          speed: features.speed,
          diskUsage: features.diskUsage,
          workspaces: features.workspaces,
          ...features,
        },
      };
    })
    .sort((a, b) => {
      // Sort by recommendation, then by speed
      if (a.recommended && !b.recommended) return -1;
      if (!a.recommended && b.recommended) return 1;
 
      const speedOrder = { "ultra-fast": 4, fastest: 3, fast: 2, medium: 1 };
      return (
        (speedOrder[b.features.speed] || 0) -
        (speedOrder[a.features.speed] || 0)
      );
    });
}

Add Package Manager Selection

File: src/prompts/steps/package-manager-step.js

Create or update the package manager selection step:

import { BaseStep } from "./base-step.js";
import {
  detectPackageManagers,
  comparePackageManagers,
} from "../../utils/process/package-managers.js";
 
export class PackageManagerStep extends BaseStep {
  constructor() {
    super("packageManager", "Which package manager would you like to use?");
  }
 
  async getChoices(previousAnswers) {
    const detection = await detectPackageManagers(false);
    const comparison = comparePackageManagers(detection.managers);
 
    return comparison.map((manager) => ({
      name: this.formatManagerChoice(manager),
      value: manager.name,
      short: manager.name,
    }));
  }
 
  formatManagerChoice(manager) {
    const { name, version, features, recommended } = manager;
    let choice = `${name} ${version}`;
 
    // Add feature highlights
    const highlights = [];
    if (features.speed === "ultra-fast" || features.speed === "fastest") {
      highlights.push("โšก Fast");
    }
    if (features.diskUsage === "low") {
      highlights.push("๐Ÿ’พ Space efficient");
    }
    if (features.runtime) {
      highlights.push("๐Ÿƒ Runtime included");
    }
    if (features.pnp) {
      highlights.push("๐Ÿ”Œ Plug'n'Play");
    }
 
    if (highlights.length > 0) {
      choice += ` (${highlights.join(", ")})`;
    }
 
    if (recommended) {
      choice += " [Recommended]";
    }
 
    return choice;
  }
 
  async getDefault(previousAnswers) {
    const detection = await detectPackageManagers(false);
    return detection.recommended;
  }
 
  validate(answer) {
    const validManagers = ["npm", "yarn", "pnpm", "bun"];
    return validManagers.includes(answer);
  }
}

Update Installation Process

File: src/utils/process/dependency-installer.js

Update the dependency installation to support new package managers:

import { PACKAGE_MANAGER_COMMANDS } from "./package-managers.js";
 
export class DependencyInstaller {
  constructor(packageManager, projectPath) {
    this.packageManager = packageManager;
    this.projectPath = projectPath;
    this.commands = PACKAGE_MANAGER_COMMANDS[packageManager];
 
    if (!this.commands) {
      throw new Error(`Unsupported package manager: ${packageManager}`);
    }
  }
 
  async installDependencies(dependencies, devDependencies) {
    const tasks = [];
 
    // Install production dependencies
    if (dependencies && Object.keys(dependencies).length > 0) {
      const deps = Object.entries(dependencies)
        .map(([name, version]) => `${name}@${version}`)
        .join(" ");
 
      tasks.push({
        title: `Installing dependencies with ${this.packageManager}`,
        command: `${this.commands.installProd} ${deps}`,
        cwd: this.projectPath,
      });
    }
 
    // Install development dependencies
    if (devDependencies && Object.keys(devDependencies).length > 0) {
      const devDeps = Object.entries(devDependencies)
        .map(([name, version]) => `${name}@${version}`)
        .join(" ");
 
      tasks.push({
        title: `Installing dev dependencies with ${this.packageManager}`,
        command: `${this.commands.installDev} ${devDeps}`,
        cwd: this.projectPath,
      });
    }
 
    // Execute installation tasks
    for (const task of tasks) {
      await this.executeCommand(task);
    }
  }
 
  async executeCommand({ title, command, cwd }) {
    UI_UTILS.log(title);
 
    try {
      await execa.command(command, {
        cwd,
        stdio: "inherit",
      });
    } catch (error) {
      throw new Error(`Failed to execute: ${command}\n${error.message}`);
    }
  }
 
  // Package manager specific optimizations
  async optimizeInstallation() {
    switch (this.packageManager) {
      case "pnpm":
        // pnpm specific optimizations
        await this.createPnpmConfig();
        break;
      case "bun":
        // Bun specific optimizations
        await this.createBunConfig();
        break;
      case "yarn":
        // Yarn specific optimizations
        await this.createYarnConfig();
        break;
    }
  }
 
  async createPnpmConfig() {
    const pnpmConfig = `
# pnpm configuration
strict-peer-dependencies=false
auto-install-peers=true
shamefully-hoist=false
    `;
 
    const configPath = path.join(this.projectPath, ".npmrc");
    await CORE_UTILS.writeFile(configPath, pnpmConfig.trim());
  }
 
  async createBunConfig() {
    // Bun configuration if needed
    const bunConfig = {
      install: {
        cache: true,
        frozenLockfile: false,
      },
    };
 
    const configPath = path.join(this.projectPath, "bunfig.toml");
    await CORE_UTILS.writeFile(
      configPath,
      `
[install]
cache = true
frozenLockfile = false
    `.trim()
    );
  }
 
  async createYarnConfig() {
    // Check if it's Yarn Berry and create appropriate config
    const yarnVersion = await this.getYarnVersion();
    const majorVersion = parseInt(yarnVersion.split(".")[0]);
 
    if (majorVersion >= 2) {
      // Yarn Berry configuration
      const yarnRc = `
nodeLinker: node-modules
enableGlobalCache: true
compressionLevel: mixed
      `;
 
      const configPath = path.join(this.projectPath, ".yarnrc.yml");
      await CORE_UTILS.writeFile(configPath, yarnRc.trim());
    }
  }
}

Update Script Generation

File: src/builders/package-json-builder.js

Update script generation to use the correct package manager commands:

generateScripts(userChoices) {
  const { framework, packageManager } = userChoices
  const commands = PACKAGE_MANAGER_COMMANDS[packageManager]
 
  const baseScripts = {
    dev: framework === 'vite' ? 'vite' : 'next dev',
    build: framework === 'vite' ? 'vite build' : 'next build',
    preview: framework === 'vite' ? 'vite preview' : 'next start'
  }
 
  // Add package manager specific scripts
  const pmScripts = {}
 
  switch (packageManager) {
    case 'pnpm':
      pmScripts['clean'] = 'pnpm store prune'
      pmScripts['update-deps'] = 'pnpm update --latest'
      break
    case 'bun':
      pmScripts['clean'] = 'bun pm cache rm'
      pmScripts['update-deps'] = 'bun update'
      if (userChoices.testing === 'bun') {
        pmScripts['test'] = 'bun test'
      }
      break
    case 'yarn':
      pmScripts['clean'] = 'yarn cache clean'
      pmScripts['update-deps'] = 'yarn upgrade-interactive --latest'
      break
  }
 
  return { ...baseScripts, ...pmScripts }
}
 
// Generate package manager specific next steps
generateNextSteps(projectName, userChoices) {
  const { packageManager } = userChoices
  const commands = PACKAGE_MANAGER_COMMANDS[packageManager]
 
  return [
    `cd ${projectName}`,
    `${commands.install}`,
    `${commands.run} dev`
  ]
}

Add to QA System

File: qa-automation/test-matrix-generator.js

Add package manager options to the test matrix:

const CONFIG_OPTIONS = {
  framework: ["vite", "nextjs"],
  packageManager: ["npm", "yarn", "pnpm", "bun"],
  typescript: [true, false],
  // ... other options
};
 
// Add package manager specific validation
const PACKAGE_MANAGER_VALIDATION = {
  npm: {
    requiredFiles: ["package-lock.json"],
    lockfileFormat: "json",
  },
  yarn: {
    requiredFiles: ["yarn.lock"],
    lockfileFormat: "yaml",
  },
  pnpm: {
    requiredFiles: ["pnpm-lock.yaml"],
    lockfileFormat: "yaml",
    configFiles: [".npmrc"],
  },
  bun: {
    requiredFiles: ["bun.lockb"],
    lockfileFormat: "binary",
    configFiles: ["bunfig.toml"],
  },
};
 
// Validate package manager setup
async function validatePackageManager(projectPath, packageManager) {
  const validation = PACKAGE_MANAGER_VALIDATION[packageManager];
 
  for (const file of validation.requiredFiles) {
    const filePath = path.join(projectPath, file);
    if (!(await CORE_UTILS.fileExists(filePath))) {
      throw new Error(`Missing ${packageManager} lockfile: ${file}`);
    }
  }
 
  // Validate package.json scripts use correct commands
  const packageJson = JSON.parse(
    await CORE_UTILS.readFile(path.join(projectPath, "package.json"))
  );
  const commands = PACKAGE_MANAGER_COMMANDS[packageManager];
 
  // Check if scripts use the correct package manager
  for (const [scriptName, scriptCommand] of Object.entries(
    packageJson.scripts
  )) {
    if (scriptCommand.includes("npm ") && packageManager !== "npm") {
      console.warn(
        `Script "${scriptName}" uses npm but ${packageManager} is configured`
      );
    }
  }
}

๐Ÿงช Testing Your Integration

Manual Testing

Test your package manager integration:

# Test with pnpm
node bin/react-kickstart.js test-pnpm --yes --framework vite --package-manager pnpm
 
# Test with Bun
node bin/react-kickstart.js test-bun --yes --framework nextjs --package-manager bun --typescript
 
# Test detection
node -e "
const { detectPackageManagers } = require('./src/utils/process/package-managers.js')
detectPackageManagers(true).then(console.log)
"

Verify Installation

cd test-pnpm
pnpm install  # Should work correctly
pnpm run dev  # Should start development server

QA Automation

# Test all package managers
node qa-automation/test-runner.js critical --package-managers pnpm,bun

๐Ÿ“š Best Practices

Performance: Different package managers have different performance characteristics. Provide users with information to make informed choices.

  • Detection Reliability - Ensure robust detection that handles edge cases
  • Command Mapping - Maintain accurate command mappings for all operations
  • Configuration - Generate appropriate configuration files for each package manager
  • Error Handling - Provide clear error messages for package manager issues
  • Performance - Consider the performance implications of different package managers

๐Ÿ“š Next Steps

Related Guides:

Package Manager Ecosystem: Your package manager integration gives developers more choice and flexibility in their development workflow.