Deployment Build Process - aku11i/phantom GitHub Wiki

Build Process

This document describes how to build Phantom for distribution and the build system architecture.

Build Overview

Phantom uses a custom TypeScript build script with esbuild for fast, efficient builds.

Build Stack

  • Build Tool: esbuild (extremely fast JavaScript bundler)
  • Language: TypeScript
  • Target: Node.js v22+
  • Module Format: ES Modules (ESM)
  • Output: Self-contained executable

Build Script

Source: build.ts

The build process is defined in a TypeScript file:

import { build } from "esbuild";
import { chmod, mkdir } from "node:fs/promises";
import { dirname } from "node:path";

async function main() {
  const entryPoint = "src/bin/phantom.ts";
  const outfile = "dist/phantom.js";

  // Ensure output directory exists
  await mkdir(dirname(outfile), { recursive: true });

  // Build with esbuild
  await build({
    entryPoints: [entryPoint],
    outfile,
    bundle: true,
    platform: "node",
    target: "node22",
    format: "esm",
    sourcemap: true,
    minify: false,
    banner: {
      js: "#!/usr/bin/env node",
    },
  });

  // Make executable
  await chmod(outfile, 0o755);
}

Build Configuration

esbuild Options

Option Value Purpose
bundle true Bundle all dependencies into single file
platform "node" Target Node.js environment
target "node22" Use Node.js v22 features
format "esm" Output ES modules
sourcemap true Generate source maps for debugging
minify false Keep code readable
banner #!/usr/bin/env node Make file executable

Output Structure

dist/
├── phantom.js      # Main executable (bundled)
└── phantom.js.map  # Source map for debugging

Running the Build

Basic Build

# Run build
pnpm build

# Or directly
npx tsx build.ts

Build Output

$ pnpm build
Building Phantom...
✓ Built dist/phantom.js (150KB)
✓ Made executable
Build completed in 0.2s

Build Process Details

1. Dependency Resolution

esbuild automatically:

  • Resolves all imports
  • Bundles TypeScript files
  • Handles node_modules (development only)
  • Respects package.json exports

2. TypeScript Compilation

  • TypeScript is compiled by esbuild
  • No separate tsc step needed
  • Type checking happens separately
  • Fast incremental builds

3. Module Bundling

Input:                     Output:
src/                       dist/phantom.js
├── bin/phantom.ts   →     (single bundled file)
├── cli/handlers/*
├── core/worktree/*
└── core/git/*

4. Executable Creation

The build script:

  1. Adds shebang (#!/usr/bin/env node)
  2. Sets executable permissions (chmod 755)
  3. Creates ready-to-run CLI tool

Build Optimizations

Why No Minification?

minify: false  // Keep readable for:
  1. Debugging - Stack traces remain useful
  2. Size - Already small (~150KB)
  3. Performance - Negligible runtime difference
  4. Security - No sensitive code to hide

Bundle Size Analysis

Check what's included:

# Analyze bundle
npx esbuild src/bin/phantom.ts --bundle --analyze

# Output shows:
# - File sizes
# - Included modules
# - Tree shaking results

Development vs Production Builds

Development Build

Current build configuration is optimized for development:

{
  sourcemap: true,   // Debugging support
  minify: false,     // Readable output
}

Production Build (if needed)

For production optimization:

{
  sourcemap: false,  // Smaller size
  minify: true,      // Compressed output
  treeShaking: true, // Remove dead code
}

Build Integration

package.json Scripts

Source: package.json#L13-L14

{
  "scripts": {
    "build": "tsx build.ts",
    "prepublishOnly": "pnpm run build"
  }
}

Automatic Builds

  • On publish: prepublishOnly runs build
  • CI/CD: Build runs in GitHub Actions
  • Local dev: Manual pnpm build

Cross-Platform Considerations

File Paths

The build handles paths correctly:

// Use Node.js path module
import { dirname, join } from "node:path";

// Not hard-coded paths
const outfile = join("dist", "phantom.js");

Line Endings

  • Build outputs LF line endings
  • Git configuration ensures consistency
  • Works on Windows, macOS, Linux

Executable Permissions

// Unix-style permissions
await chmod(outfile, 0o755);

// Equivalent to: -rwxr-xr-x
// Owner: read, write, execute
// Group: read, execute
// Others: read, execute

Build Artifacts

What's Included

The build includes:

  • All source code from src/
  • Type definitions (compiled away)
  • No runtime dependencies
  • No development dependencies

What's Excluded

Not bundled:

  • Test files (*.test.ts)
  • Development dependencies
  • TypeScript source files
  • Documentation

Build Performance

Metrics

Typical build times:

  • Full build: ~200ms
  • Incremental: ~50ms
  • Watch mode: Instant

Why esbuild?

Compared to other bundlers:

Tool Build Time Features
esbuild 0.2s Fast, simple
Webpack 3-5s Complex, plugins
Rollup 2-3s Tree shaking
tsc 1-2s Type checking only

Troubleshooting Builds

Common Issues

1. Build Fails

# Check Node version
node --version  # Must be v22+

# Clear and rebuild
rm -rf dist
pnpm build

2. Import Errors

// ✓ Correct - with extension
import { something } from "./module.js";

// ✗ Wrong - missing extension
import { something } from "./module";

3. Module Not Found

# Ensure dependencies installed
pnpm install

# Check import paths
# Must be relative or from node_modules

Debug Build Issues

# Verbose esbuild output
npx esbuild src/bin/phantom.ts \
  --bundle \
  --platform=node \
  --log-level=debug

# Check bundled output
cat dist/phantom.js | less

Custom Build Tasks

Adding Pre-build Steps

// In build.ts
async function prebuild() {
  // Clean dist directory
  await rm("dist", { recursive: true, force: true });
  
  // Generate version info
  await generateVersion();
}

async function main() {
  await prebuild();
  await build({ /* ... */ });
}

Post-build Steps

async function postbuild() {
  // Copy additional files
  await copyFile("LICENSE", "dist/LICENSE");
  
  // Generate metadata
  await writeFile("dist/metadata.json", JSON.stringify({
    version,
    buildTime: new Date().toISOString(),
  }));
}

CI/CD Build Process

GitHub Actions Build

The CI pipeline runs:

- name: Build
  run: pnpm build

- name: Verify build
  run: |
    test -f dist/phantom.js
    test -x dist/phantom.js

Release Builds

For npm releases:

# Version bump
npm version patch

# Build and publish
npm publish
# prepublishOnly automatically runs build

Build Best Practices

1. Keep Builds Fast

  • Use esbuild for speed
  • Avoid unnecessary transformations
  • Cache when possible

2. Reproducible Builds

  • Lock dependencies (pnpm-lock.yaml)
  • Specify exact Node version
  • Use consistent environment

3. Build Validation

Always verify builds:

# Test the built executable
./dist/phantom.js --version
./dist/phantom.js list

# Check file size
ls -lh dist/phantom.js

4. Source Maps

Include in development:

  • Debugging support
  • Error stack traces
  • Performance profiling

Advanced Build Topics

Custom esbuild Plugins

// Example: Add build info
const buildInfoPlugin = {
  name: "build-info",
  setup(build) {
    build.onResolve({ filter: /^BUILD_INFO$/ }, () => ({
      path: "BUILD_INFO",
      namespace: "build-info",
    }));
    
    build.onLoad({ filter: /.*/, namespace: "build-info" }, () => ({
      contents: JSON.stringify({
        version: process.env.npm_package_version,
        buildTime: new Date().toISOString(),
      }),
      loader: "json",
    }));
  },
};

// Use in build
await build({
  plugins: [buildInfoPlugin],
  // ... other options
});

Watch Mode

For development:

// Add to build.ts
if (process.argv.includes("--watch")) {
  const ctx = await context({
    // ... build options
  });
  
  await ctx.watch();
  console.log("Watching for changes...");
}

Summary

Phantom's build process is:

  1. Fast - Sub-second builds with esbuild
  2. Simple - Single TypeScript file configuration
  3. Reliable - Consistent, reproducible outputs
  4. Efficient - Small, self-contained executables
  5. Developer-friendly - Source maps and readable output

The build system prioritizes developer experience while producing efficient, distributable CLI tools.