nvenv
Version:
Python venv-like Node.js environment manager - project-local Node.js installation without global environment pollution
605 lines (448 loc) • 18.5 kB
Markdown
# CLAUDE.md - Context for AI Assistants
This document contains technical context and design decisions for AI assistants working on this project.
## Project Overview
**nvenv** is a Node.js virtual environment manager inspired by Python's venv. It creates project-local Node.js installations without global environment pollution, following Gradle's philosophy of project-local dependency management.
## Design Philosophy
### Core Principles
1. **Project-local installation**: Each environment is completely isolated within the project directory
2. **No global state**: Unlike nvm/asdf/mise, no shared cache or global version registry
3. **Zero dependencies**: Uses only Node.js standard library (https, fs, path, child_process)
4. **Storage redundancy is acceptable**: Disk space trade-off for complete isolation
5. **Python venv-like UX**: Familiar activate/deactivate workflow
### Why Not Global Version Managers?
Traditional tools (nvm, asdf, mise, volta) manage Node.js versions globally:
- Shared state across projects leads to version conflicts
- Implicit dependencies on global environment
- Difficult to reproduce exact environment on different machines
nvenv solves this by treating Node.js like project dependencies (similar to node_modules).
## Architecture
### Module Dependency Graph
```
cli.js (entry point)
└─> env.js (orchestration)
├─> platform.js (detect OS/arch, build URLs, platform-specific helpers)
├─> download.js (fetch Node.js binaries)
├─> extract.js (unpack archives)
└─> utils.js (shared utilities: logging, silent mode, platform checks)
```
### Module Responsibilities
#### `src/utils.js`
- **Purpose**: Shared utility functions used across all modules
- **Key functions**:
- `shouldBeSilent(options)`: Centralized silent mode detection
- `isWindows()`: Platform detection helper
- `log()`, `warn()`, `error()`: Logging helpers with silent mode support
- **Design rationale**: Eliminates code duplication (DRY principle)
#### `src/platform.js`
- Detects `process.platform` and `process.arch`
- Maps to Node.js download URL conventions
- Supported: darwin/linux/win32 on x64/arm64
- Returns download info: `{ url, filename, extension, platform, arch, version }`
- Platform-specific helpers:
- `getNodeBinDir(nodePath)`: Returns correct bin directory for platform
- `getExecutableExtensions()`: Returns platform-specific executable extensions
- `isExecutableFile(filename, binDir)`: Determines if file is executable
**Key implementation detail**: Version normalization removes 'v' prefix if present.
#### `src/download.js`
- HTTPS download with progress indicator
- Handles 301/302 redirects automatically
- Progress display: percentage, downloaded MB, total MB
- Atomic operations: deletes partially downloaded files on error
- Uses Node.js core `https` module (no axios/got)
**Key implementation detail**: Follows redirects recursively, essential for nodejs.org CDN.
#### `src/extract.js`
- Platform-specific extraction:
- Unix: `tar -xzf` for .tar.gz
- Windows: PowerShell `Expand-Archive` for .zip
- Fallback: `unzip` command on Unix
- Locates extracted directory (pattern: `node-v*`)
- Uses `execSync` for synchronous execution
**Key implementation detail**: Archive extracts to subdirectory; `findNodeDirectory()` locates it.
#### `src/env.js`
- Orchestrates complete environment creation
- Directory structure:
```
venv/
├── bin/ # Symlinks and activate scripts
├── lib/ # Actual Node.js installation
└── .nvenv # Metadata JSON
```
- **Windows**: Copies entire Node.js installation to `bin/` (Python venv approach)
- **Unix**: Creates symlinks for node, npm, npx executables
- Generates activate scripts for bash/zsh and fish
- Saves metadata: version, platform, arch, creation timestamp
- Uses shared utilities from `utils.js` for consistent logging and platform detection
- Uses platform helpers from `platform.js` for cross-platform compatibility
**Key implementation detail**: Windows uses full copy instead of symlinks because:
1. npm.cmd/npx.cmd use `%~dp0` to locate node_modules, which breaks with symlinks/wrappers
2. Symlinks on Windows require admin privileges or Developer Mode
3. Python's venv uses the same approach with proven reliability
#### `src/cli.js`
- Argument parsing: `--node=<version> <path>`
- Help message with `-h` or `--help`
- Error handling and user-friendly output
- Executable via shebang: `#!/usr/bin/env node`
### Activate Scripts
Generated scripts modify shell environment:
**Bash/Zsh** (`bin/activate`):
- Prepends `venv/bin` to `$PATH`
- Saves original `$PATH` in `$_OLD_VIRTUAL_PATH`
- Updates `$PS1` prompt with `(nvenv)` prefix
- Defines `deactivate()` function to restore environment
**Fish** (`bin/activate.fish`):
- Sets `PATH` prepending `venv/bin`
- Overrides `fish_prompt` function
- Defines `deactivate` function
**Critical**: Scripts must be sourced (`source venv/bin/activate`), not executed directly.
## Implementation Details
### Node.js Download URLs
Pattern: `https://nodejs.org/dist/v{version}/node-v{version}-{platform}-{arch}.{ext}`
Examples:
- macOS ARM64: `node-v18.20.0-darwin-arm64.tar.gz`
- Linux x64: `node-v18.20.0-linux-x64.tar.gz`
- Windows x64: `node-v18.20.0-win32-x64.zip`
### Binary Linking Strategy
**Unix (macOS/Linux)**: Symlinks to versioned installation
```
bin/node -> ../lib/node-v18.20.0/bin/node
bin/npm -> ../lib/node-v18.20.0/bin/npm
```
**Windows**: Full copy of Node.js installation
```
bin/
node.exe (copied from lib/node-v18.20.0/)
npm.cmd (copied)
npx.cmd (copied)
node_modules/ (copied)
[all other files] (copied)
```
Benefits of this approach:
- **Unix**: Multiple versions can coexist in `lib/`, symlinks enable easy switching
- **Windows**: npm.cmd works correctly (uses %~dp0 to find node_modules)
- Follows Python venv's proven cross-platform strategy
### Metadata Format
`.nvenv` JSON structure:
```json
{
"version": "18.20.0",
"platform": "darwin",
"arch": "arm64",
"created": "2025-10-19T18:00:10.822Z"
}
```
Purpose: Future features like version upgrades, environment inspection.
## Configuration and Environment Variables
### Supported Environment Variables
nvenv supports the following environment variables for configuration:
#### `NVENV_SILENT`
Suppresses verbose progress output during environment creation.
**Values**: `1`, `true`, or any truthy value
**Default**: `false` (verbose output enabled)
**Scope**: Affects download progress, extraction messages, and all non-error output
**Usage**:
```bash
NVENV_SILENT=1 npx nvenv --node=18.20.0 venv
```
**Implementation**: The `shouldBeSilent()` helper in all modules checks:
1. Explicit `options.silent` parameter (takes precedence)
2. `NVENV_SILENT` environment variable
3. `NODE_ENV === 'test'` (auto-enables silent mode in tests)
**Affected modules**: `src/download.js`, `src/extract.js`, `src/env.js`, `src/cli.js`
#### `NVENV_MIRROR`
Specifies a custom mirror URL for downloading Node.js binaries.
**Values**: Any valid URL (without trailing slash)
**Default**: `https://nodejs.org/dist`
**Scope**: Affects download URL generation in `src/platform.js`
**Usage**:
```bash
# China mirror (npmmirror/Taobao)
NVENV_MIRROR=https://npmmirror.com/mirrors/node npx nvenv --node=18.20.0 venv
# Tencent Cloud mirror
NVENV_MIRROR=https://mirrors.cloud.tencent.com/nodejs-release npx nvenv --node=18.20.0 venv
```
**Implementation**: In `getNodeDownloadInfo()` function:
```javascript
const baseUrl = process.env.NVENV_MIRROR || 'https://nodejs.org/dist';
const url = `${baseUrl}/v${normalizedVersion}/${filename}.${ext}`;
```
**URL Structure**: The mirror must follow the same directory structure as nodejs.org:
```
${NVENV_MIRROR}/v${version}/node-v${version}-${platform}-${arch}.${ext}
```
Example: `https://npmmirror.com/mirrors/node/v18.20.0/node-v18.20.0-darwin-arm64.tar.gz`
**Popular Mirrors**:
- **China (npmmirror)**: `https://npmmirror.com/mirrors/node`
- **China (Tencent)**: `https://mirrors.cloud.tencent.com/nodejs-release`
- **Corporate/Private**: Custom internal mirrors following the same structure
**Performance Impact**: Using a geographically closer mirror can significantly reduce download times:
- Official nodejs.org: ~30-60 seconds for 40MB binary
- Regional mirror: ~5-15 seconds for the same binary
- Results vary based on network conditions and mirror performance
### CLI Flags
In addition to environment variables, nvenv supports these command-line flags:
- `--node=<version>`: Node.js version to install (required)
- `--silent` or `-s`: Suppress progress output (same as `NVENV_SILENT=1`)
- `--help` or `-h`: Show help message
### Configuration Priority
When multiple configuration sources are present, the priority order is:
1. CLI flags (e.g., `--silent`)
2. Environment variables (e.g., `NVENV_SILENT=1`)
3. Auto-detection (e.g., `NODE_ENV=test`)
4. Default values
## Known Limitations
1. **No Windows PowerShell activate script**: Only bash/zsh/fish supported
2. **No version caching**: Each environment downloads Node.js independently
3. **No .nvmrc support**: Version must be specified via `--node=`
4. **No npm version control**: Uses npm bundled with Node.js
5. **No environment migration**: Cannot upgrade existing environment to new Node.js version
## Future Enhancement Considerations
### If Adding Global Cache
Trade-offs:
- **Pro**: Reduces disk usage, faster environment creation
- **Con**: Violates "no global state" principle
- **Implementation**: Store in `~/.cache/nvenv/{version}-{platform}-{arch}/`
- **Symlink strategy**: Link from `venv/lib/` to cache
### If Adding .nvmrc Support
```javascript
function readVersionFromNvmrc() {
const nvmrcPath = path.join(process.cwd(), '.nvmrc');
if (fs.existsSync(nvmrcPath)) {
return fs.readFileSync(nvmrcPath, 'utf8').trim();
}
return null;
}
```
Fallback order: `--node=` flag > `.nvmrc` > error
### If Adding npm/pnpm/yarn Version Control
Download from:
- npm: `https://registry.npmjs.org/npm/-/npm-{version}.tgz`
- pnpm: `https://github.com/pnpm/pnpm/releases/download/v{version}/pnpm-{platform}-{arch}`
- yarn: `https://github.com/yarnpkg/yarn/releases/download/v{version}/yarn-v{version}.tar.gz`
Install to `venv/lib/` and update symlinks.
### If Adding Windows PowerShell Support
Template for `bin/activate.ps1`:
```powershell
$env:_OLD_VIRTUAL_PATH = $env:PATH
$env:PATH = "$PSScriptRoot;$env:PATH"
$env:NVENV = (Get-Item $PSScriptRoot).Parent.FullName
function deactivate {
$env:PATH = $env:_OLD_VIRTUAL_PATH
Remove-Item Env:_OLD_VIRTUAL_PATH
Remove-Item Env:NVENV
}
```
## Testing Strategy
### Test Organization
Tests are organized into two categories:
**Unit Tests** (`test/*.test.js`):
- Fast, isolated tests of individual functions
- Mock external dependencies where possible
- Run with: `npm run test:unit`
- Files:
- `cli.test.js`: CLI argument parsing
- `platform.test.js`: Platform detection and URL generation
- `download.test.js`: Download functionality (uses small real files)
- `extract.test.js`: Archive extraction
- `env.test.js`: Environment structure creation
**Integration Tests** (`test/integration/*.test.js`):
- Slow, end-to-end tests with real downloads
- Require network access and significant disk space
- Run with: `npm run test:integration`
- Files:
- `env-integration.test.js`: Full environment creation with Node.js download
- `archive-structure.test.js`: Validates Node.js archive structure across versions
**Running Tests**:
```bash
npm run test:unit # Fast unit tests only
npm run test:integration # Slow integration tests only
npm test # All tests (default)
```
### Manual Testing Checklist
1. **Environment creation**:
- `npx nvenv --node=18.20.0 test-env`
- Verify directory structure created
- Check symlinks point to correct binaries
2. **Activation**:
- `source test-env/bin/activate`
- Verify `which node` points to `test-env/bin/node`
- Verify `node --version` matches requested version
- Verify prompt shows `(nvenv)`
3. **Usage**:
- `npm install express`
- Verify packages install to `test-env/` not global
4. **Deactivation**:
- `deactivate`
- Verify `which node` returns to system node
- Verify prompt restored
5. **Multiple environments**:
- Create two environments with different versions
- Activate each, verify correct version
### Edge Cases to Test
- Invalid version number → should fail with clear error
- Network failure during download → should clean up partial download
- Insufficient disk space → should fail gracefully
- Environment path already exists → should error or prompt
- Non-existent Node.js version → nodejs.org returns 404
## Code Style and Conventions
### Error Handling
Always throw descriptive errors:
```javascript
throw new Error(`Unsupported platform: ${platform}`);
```
Never silent failures. User should understand what went wrong.
### Async/Await
Use async/await for clarity:
```javascript
async function createEnvironment(version, envPath) {
await downloadFile(url, archivePath);
await extractArchive(archivePath, extractDir);
// ...
}
```
### File Operations
Use synchronous fs for simplicity:
```javascript
fs.mkdirSync(dir, { recursive: true });
```
Only download.js uses async (network I/O).
### Console Output
User-facing messages:
- Progress: `\r` for overwrite (download progress)
- Completion: `\n` for new line
- Errors: `console.error()` with prefix "Error:"
- Success: `✓` checkmark for visual confirmation
## Dependencies
**Production**: NONE (zero dependencies)
**Runtime Requirements**:
- Node.js >= 18.0.0 (specified in package.json engines)
- Unix: `tar` command for extraction
- Windows: PowerShell for zip extraction
**System Dependencies**:
- git (for development)
- chmod (Unix, for setting executable permissions)
## NPM Package Publishing
Before publishing:
1. Test with `npm pack` to verify included files
2. Check `.npmignore` excludes test files and perplexity-report.md
3. Verify `bin` entry points to `src/cli.js`
4. Ensure `chmod +x src/cli.js` for Unix executable
Publish command:
```bash
npm publish
```
## Security Considerations
1. **HTTPS only**: All downloads over HTTPS (nodejs.org)
2. **No arbitrary code execution**: No eval(), no dynamic requires
3. **Temp file cleanup**: Always delete downloaded archives after extraction
4. **Symlink safety**: Check symlink targets exist before creating
5. **Path traversal**: Use `path.join()` and `path.resolve()` to prevent
## Performance Characteristics
- **Download**: ~40MB for typical Node.js binary (darwin-arm64)
- **Extraction**: ~2-5 seconds depending on disk I/O
- **Total time**: ~30-60 seconds on good network connection
- **Disk usage**: ~50-60MB per environment (full Node.js installation)
## Debugging Tips
### Enable verbose logging
Add to modules:
```javascript
const DEBUG = process.env.DEBUG === 'nvenv';
if (DEBUG) console.log('Debug info:', ...);
```
Usage:
```bash
DEBUG=nvenv npx nvenv --node=18.20.0 venv
```
### Check downloaded archive
Downloaded to `/tmp/node-v{version}-{platform}-{arch}.{ext}`
Verify manually:
```bash
ls -lh /tmp/node-v*.tar.gz
tar -tzf /tmp/node-v18.20.0-darwin-arm64.tar.gz | head
```
### Inspect symlinks
```bash
ls -la venv/bin/
file venv/bin/node # Should show: symbolic link to ...
```
### Test activate script
```bash
bash -x venv/bin/activate # Debug mode
```
## Common Issues and Solutions
### "tar: command not found" on minimal systems
Install tar:
- Alpine Linux: `apk add tar`
- Debian/Ubuntu: `apt install tar`
### Symlink creation fails on Windows
Falls back to wrapper scripts. Verify:
```cmd
type venv\bin\node.cmd
```
### "Permission denied" when creating environment
Ensure write permissions:
```bash
chmod -R u+w .
```
## Integration with CI/CD
### GitHub Actions Example
```yaml
- name: Create Node.js environment
run: npx nvenv --node=18.20.0 venv
- name: Run tests
run: |
source venv/bin/activate
npm install
npm test
```
### Docker Example
```dockerfile
RUN npx nvenv --node=18.20.0 /app/venv
ENV PATH="/app/venv/bin:$PATH"
RUN npm install
```
## Version Management
- Current version: v0.1.0 (unreleased)
- Future versions should maintain backward compatibility with .nvenv metadata format
## Related Projects and Inspirations
- **Python venv**: Inspiration for activate/deactivate workflow
- **Gradle**: Inspiration for project-local dependencies
- **nodeenv**: Python-based Node.js environment tool (requires Python installation)
- **nve**: Temporary execution in specific Node version (doesn't create persistent environment)
- **npm node package**: npm package containing Node.js binary (similar approach but different UX)
## When to Use vs. Alternatives
**Use nvenv when**:
- Complete project isolation required
- Reproducible environments across machines
- No global state acceptable
- Python venv-like workflow preferred
**Use nvm/asdf/mise when**:
- Multiple projects share same Node.js versions
- Disk space is constrained
- Global version switching needed
- Shell integration preferred over activation
## Maintenance Notes
### Updating for New Node.js Release Patterns
If nodejs.org changes URL structure, update `src/platform.js`:
```javascript
const url = `https://nodejs.org/dist/v${normalizedVersion}/${filename}.${ext}`;
```
### Adding New Platform Support
1. Add to `getPlatform()` and `getArch()` in platform.js
2. Update `getExtension()` if different archive format
3. Update extract.js if new extraction method needed
4. Test on target platform
### Handling Breaking Changes
If making breaking changes:
- Bump major version (semantic versioning)
- Document migration path in CHANGELOG.md
- Consider backward compatibility for .nvenv format
- Provide migration script if possible
## Coding Guidelines
1. **DRY Principle**: Shared code lives in `utils.js` or as helpers in appropriate modules
2. **Platform Abstraction**: Platform-specific logic should use helpers from `platform.js`
3. **Silent Mode**: All user-facing output should use logging helpers from `utils.js`
4. **Testing**: Unit tests for logic, integration tests for end-to-end flows
5. **Zero Dependencies**: Use only Node.js standard library
---
**Last Updated**: 2025-10-23
**Maintained By**: Claude Code (claude-sonnet-4-5-20250929)