Skip to main content

Node.js and npm Projects

psake can orchestrate Node.js and npm-based builds, providing a consistent PowerShell-based build automation layer across your development workflow. This guide shows you how to build, test, bundle, and publish Node.js projects using psake.

Quick Start

Here's a basic psake build script for a Node.js project:

Properties {
$ProjectRoot = $PSScriptRoot
$NodeModules = Join-Path $ProjectRoot 'node_modules'
$BuildDir = Join-Path $ProjectRoot 'build'
$DistDir = Join-Path $ProjectRoot 'dist'
}

Task Default -depends Test

Task Clean {
if (Test-Path $BuildDir) {
Remove-Item $BuildDir -Recurse -Force
}
if (Test-Path $DistDir) {
Remove-Item $DistDir -Recurse -Force
}
}

Task Install {
exec { npm install }
}

Task Build -depends Install, Clean {
exec { npm run build }
}

Task Test -depends Build {
exec { npm test }
}

Run the build:

Invoke-psake -buildFile .\psakefile.ps1

Complete Node.js Build Example

Here's a comprehensive psakefile.ps1 for a production Node.js application:

Properties {
$ProjectRoot = $PSScriptRoot
$SrcDir = Join-Path $ProjectRoot 'src'
$TestDir = Join-Path $ProjectRoot 'tests'
$BuildDir = Join-Path $ProjectRoot 'build'
$DistDir = Join-Path $ProjectRoot 'dist'
$CoverageDir = Join-Path $ProjectRoot 'coverage'
$NodeModules = Join-Path $ProjectRoot 'node_modules'

$Environment = 'development'
$Version = '1.0.0'
$Verbose = $false
}

FormatTaskName {
param($taskName)
Write-Host "Executing task: $taskName" -ForegroundColor Cyan
}

Task Default -depends Test

Task Clean {
Write-Host "Cleaning build artifacts..." -ForegroundColor Green

@($BuildDir, $DistDir, $CoverageDir) | ForEach-Object {
if (Test-Path $_) {
Remove-Item $_ -Recurse -Force
Write-Host " Removed: $_" -ForegroundColor Gray
}
}

New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null
New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
}

Task Install {
Write-Host "Installing npm dependencies..." -ForegroundColor Green

if (-not (Test-Path $NodeModules)) {
exec { npm install }
} else {
exec { npm ci }
}
}

Task Lint -depends Install {
Write-Host "Running ESLint..." -ForegroundColor Green
exec { npm run lint }
}

Task Build -depends Install, Clean {
Write-Host "Building application..." -ForegroundColor Green

$env:NODE_ENV = $Environment

if ($Verbose) {
exec { npm run build -- --verbose }
} else {
exec { npm run build }
}

Write-Host "Build complete: $BuildDir" -ForegroundColor Green
}

Task Test -depends Build {
Write-Host "Running tests..." -ForegroundColor Green
exec { npm test }
}

Task TestWatch {
Write-Host "Running tests in watch mode..." -ForegroundColor Green
exec { npm run test:watch }
}

Task Coverage -depends Install {
Write-Host "Running tests with coverage..." -ForegroundColor Green
exec { npm run test:coverage }

if (Test-Path (Join-Path $CoverageDir 'lcov-report/index.html')) {
Write-Host "Coverage report: $CoverageDir/lcov-report/index.html" -ForegroundColor Yellow
}
}

Task Bundle -depends Test {
Write-Host "Creating production bundle..." -ForegroundColor Green

$env:NODE_ENV = 'production'
exec { npm run bundle }

Write-Host "Bundle complete: $DistDir" -ForegroundColor Green
}

Task Package -depends Bundle {
Write-Host "Creating package..." -ForegroundColor Green

exec { npm pack --pack-destination $DistDir }

$packageFile = Get-ChildItem "$DistDir/*.tgz" | Select-Object -First 1
Write-Host "Package created: $($packageFile.Name)" -ForegroundColor Green
}

Task Verify {
Write-Host "Verifying package.json..." -ForegroundColor Green

if (-not (Test-Path 'package.json')) {
throw "package.json not found"
}

$packageJson = Get-Content 'package.json' | ConvertFrom-Json

if ([string]::IsNullOrEmpty($packageJson.name)) {
throw "Package name is required in package.json"
}

if ([string]::IsNullOrEmpty($packageJson.version)) {
throw "Package version is required in package.json"
}

Write-Host " Package: $($packageJson.name)@$($packageJson.version)" -ForegroundColor Gray
}

Task Publish -depends Test, Verify, Package {
Write-Host "Publishing to npm registry..." -ForegroundColor Green

$npmToken = $env:NPM_TOKEN
if ([string]::IsNullOrEmpty($npmToken)) {
throw "NPM_TOKEN environment variable is required for publishing"
}

# Configure npm authentication
exec { npm config set //registry.npmjs.org/:_authToken $npmToken }

try {
exec { npm publish --access public }
Write-Host "Successfully published to npm registry" -ForegroundColor Green
}
finally {
# Clean up authentication
exec { npm config delete //registry.npmjs.org/:_authToken }
}
}

Task Dev {
Write-Host "Starting development server..." -ForegroundColor Green
exec { npm run dev }
}

Task Serve -depends Build {
Write-Host "Starting production server..." -ForegroundColor Green

$env:NODE_ENV = 'production'
exec { npm start }
}

Task Audit {
Write-Host "Running security audit..." -ForegroundColor Green
exec { npm audit }
}

Task AuditFix {
Write-Host "Fixing security vulnerabilities..." -ForegroundColor Green
exec { npm audit fix }
}

Task Outdated {
Write-Host "Checking for outdated packages..." -ForegroundColor Green
exec { npm outdated } -errorMessage "Some packages are outdated (this is informational)"
}

Task UpdateDeps {
Write-Host "Updating dependencies..." -ForegroundColor Green
exec { npm update }
exec { npm outdated } -errorMessage "Dependencies updated"
}

TypeScript Compilation

For TypeScript projects, add TypeScript-specific tasks:

Properties {
$ProjectRoot = $PSScriptRoot
$SrcDir = Join-Path $ProjectRoot 'src'
$OutDir = Join-Path $ProjectRoot 'dist'
$TsConfig = Join-Path $ProjectRoot 'tsconfig.json'
}

Task TypeCheck -depends Install {
Write-Host "Running TypeScript type checking..." -ForegroundColor Green

if (-not (Test-Path $TsConfig)) {
throw "tsconfig.json not found"
}

exec { npx tsc --noEmit }
Write-Host "Type checking passed" -ForegroundColor Green
}

Task CompileTS -depends Install, Clean {
Write-Host "Compiling TypeScript..." -ForegroundColor Green

exec { npx tsc --project $TsConfig }

Write-Host "TypeScript compilation complete: $OutDir" -ForegroundColor Green
}

Task CompileTSWatch {
Write-Host "Compiling TypeScript in watch mode..." -ForegroundColor Green
exec { npx tsc --watch --project $TsConfig }
}

Task Build -depends TypeCheck, CompileTS {
Write-Host "Build complete" -ForegroundColor Green
}

Update your tsconfig.json:

{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}

Webpack Bundling

For projects using Webpack, integrate bundling tasks:

Properties {
$ProjectRoot = $PSScriptRoot
$SrcDir = Join-Path $ProjectRoot 'src'
$DistDir = Join-Path $ProjectRoot 'dist'
$WebpackConfig = Join-Path $ProjectRoot 'webpack.config.js'
$Environment = 'development'
}

Task WebpackBuild -depends Install {
Write-Host "Running Webpack build ($Environment)..." -ForegroundColor Green

if (-not (Test-Path $WebpackConfig)) {
throw "webpack.config.js not found"
}

$env:NODE_ENV = $Environment

if ($Environment -eq 'production') {
exec { npx webpack --config $WebpackConfig --mode production }
} else {
exec { npx webpack --config $WebpackConfig --mode development }
}

Write-Host "Webpack bundle complete: $DistDir" -ForegroundColor Green
}

Task WebpackWatch -depends Install {
Write-Host "Running Webpack in watch mode..." -ForegroundColor Green
exec { npx webpack --config $WebpackConfig --mode development --watch }
}

Task WebpackAnalyze -depends Install {
Write-Host "Analyzing Webpack bundle..." -ForegroundColor Green

$env:ANALYZE = 'true'
exec { npx webpack --config $WebpackConfig --mode production }
}

Task OptimizeBundle -depends WebpackBuild {
Write-Host "Optimizing bundle size..." -ForegroundColor Green

# Run bundle size analysis
exec { npx bundlesize }

# Check bundle sizes
$jsFiles = Get-ChildItem "$DistDir/*.js" -File
foreach ($file in $jsFiles) {
$sizeKB = [math]::Round($file.Length / 1KB, 2)
Write-Host " $($file.Name): ${sizeKB} KB" -ForegroundColor Gray

if ($sizeKB -gt 500) {
Write-Warning "Bundle size exceeds 500 KB: $($file.Name)"
}
}
}

Example webpack.config.js:

const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
plugins: [
...(process.env.ANALYZE ? [new BundleAnalyzerPlugin()] : []),
],
};

Testing with Jest

Integrate Jest testing into your psake build:

Properties {
$ProjectRoot = $PSScriptRoot
$TestDir = Join-Path $ProjectRoot 'tests'
$CoverageDir = Join-Path $ProjectRoot 'coverage'
$CoverageThreshold = 80
}

Task Test -depends Install {
Write-Host "Running Jest tests..." -ForegroundColor Green
exec { npx jest --coverage=false }
}

Task TestWatch -depends Install {
Write-Host "Running Jest in watch mode..." -ForegroundColor Green
exec { npx jest --watch }
}

Task TestCoverage -depends Install {
Write-Host "Running tests with coverage..." -ForegroundColor Green
exec { npx jest --coverage --coverageReporters=text --coverageReporters=html }

# Parse coverage summary
$coverageSummary = Join-Path $CoverageDir 'coverage-summary.json'
if (Test-Path $coverageSummary) {
$coverage = Get-Content $coverageSummary | ConvertFrom-Json
$totalCoverage = $coverage.total.lines.pct

Write-Host "Total line coverage: ${totalCoverage}%" -ForegroundColor Cyan

if ($totalCoverage -lt $CoverageThreshold) {
throw "Coverage ${totalCoverage}% is below threshold ${CoverageThreshold}%"
}
}
}

Task TestCI -depends Install {
Write-Host "Running tests for CI..." -ForegroundColor Green

# Use CI-friendly options
exec { npx jest --ci --coverage --maxWorkers=2 }
}

Example jest.config.js:

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};

Linting with ESLint

Add linting tasks to maintain code quality:

Task Lint -depends Install {
Write-Host "Running ESLint..." -ForegroundColor Green
exec { npx eslint src --ext .js,.ts,.tsx }
}

Task LintFix -depends Install {
Write-Host "Running ESLint with auto-fix..." -ForegroundColor Green
exec { npx eslint src --ext .js,.ts,.tsx --fix }
}

Task Format -depends Install {
Write-Host "Formatting code with Prettier..." -ForegroundColor Green
exec { npx prettier --write "src/**/*.{js,ts,tsx,json,css,md}" }
}

Task FormatCheck -depends Install {
Write-Host "Checking code formatting..." -ForegroundColor Green
exec { npx prettier --check "src/**/*.{js,ts,tsx,json,css,md}" }
}

Publishing to npm Registry

Here's a complete workflow for publishing packages to npm:

Properties {
$ProjectRoot = $PSScriptRoot
$DistDir = Join-Path $ProjectRoot 'dist'
$Registry = 'https://registry.npmjs.org/'
$NpmToken = $env:NPM_TOKEN
$DryRun = $false
}

Task ValidatePackage {
Write-Host "Validating package..." -ForegroundColor Green

# Check package.json
if (-not (Test-Path 'package.json')) {
throw "package.json not found"
}

$pkg = Get-Content 'package.json' | ConvertFrom-Json

# Validate required fields
$requiredFields = @('name', 'version', 'description', 'main', 'license')
foreach ($field in $requiredFields) {
if ([string]::IsNullOrEmpty($pkg.$field)) {
throw "package.json is missing required field: $field"
}
}

# Check if version already exists
$packageName = $pkg.name
$version = $pkg.version

Write-Host " Package: $packageName" -ForegroundColor Gray
Write-Host " Version: $version" -ForegroundColor Gray
Write-Host " License: $($pkg.license)" -ForegroundColor Gray

try {
$existingVersions = npm view $packageName versions --json | ConvertFrom-Json
if ($existingVersions -contains $version) {
throw "Version $version already exists in registry"
}
}
catch {
Write-Host " Package not yet published (this is OK for first release)" -ForegroundColor Yellow
}
}

Task PrepareRelease -depends Test, ValidatePackage {
Write-Host "Preparing release..." -ForegroundColor Green

# Clean and build
exec { Invoke-psake -taskList Clean, Build }

# Verify dist directory exists
if (-not (Test-Path $DistDir)) {
throw "Distribution directory not found: $DistDir"
}

# Check for required files
$requiredFiles = @('package.json', 'README.md')
foreach ($file in $requiredFiles) {
if (-not (Test-Path $file)) {
throw "Required file not found: $file"
}
}
}

Task PublishPackage -depends PrepareRelease {
Write-Host "Publishing package to npm..." -ForegroundColor Green

if ([string]::IsNullOrEmpty($NpmToken)) {
throw "NPM_TOKEN environment variable is required"
}

# Configure authentication
$npmrcPath = Join-Path $ProjectRoot '.npmrc'
try {
# Create temporary .npmrc
@"
//registry.npmjs.org/:_authToken=$NpmToken
registry=$Registry
"@ | Set-Content $npmrcPath

if ($DryRun) {
Write-Host "DRY RUN: Would publish package" -ForegroundColor Yellow
exec { npm publish --dry-run --access public }
}
else {
exec { npm publish --access public }

$pkg = Get-Content 'package.json' | ConvertFrom-Json
Write-Host "Successfully published $($pkg.name)@$($pkg.version)" -ForegroundColor Green
}
}
finally {
# Clean up .npmrc
if (Test-Path $npmrcPath) {
Remove-Item $npmrcPath -Force
}
}
}

Task PublishBeta -depends Test, ValidatePackage {
Write-Host "Publishing beta version to npm..." -ForegroundColor Green

if ([string]::IsNullOrEmpty($NpmToken)) {
throw "NPM_TOKEN environment variable is required"
}

# Configure authentication
exec { npm config set //registry.npmjs.org/:_authToken $NpmToken }

try {
exec { npm publish --tag beta --access public }
Write-Host "Successfully published beta version" -ForegroundColor Green
}
finally {
exec { npm config delete //registry.npmjs.org/:_authToken }
}
}

Task UnpublishPackage {
Write-Host "WARNING: Unpublishing package..." -ForegroundColor Red

$pkg = Get-Content 'package.json' | ConvertFrom-Json
$packageName = $pkg.name
$version = $pkg.version

$confirmation = Read-Host "Are you sure you want to unpublish ${packageName}@${version}? (yes/no)"
if ($confirmation -ne 'yes') {
Write-Host "Unpublish cancelled" -ForegroundColor Yellow
return
}

if ([string]::IsNullOrEmpty($NpmToken)) {
throw "NPM_TOKEN environment variable is required"
}

exec { npm config set //registry.npmjs.org/:_authToken $NpmToken }

try {
exec { npm unpublish "${packageName}@${version}" }
Write-Host "Successfully unpublished ${packageName}@${version}" -ForegroundColor Green
}
finally {
exec { npm config delete //registry.npmjs.org/:_authToken }
}
}

Monorepo Support (npm Workspaces)

For monorepo projects using npm workspaces:

Properties {
$ProjectRoot = $PSScriptRoot
$Workspaces = @('packages/core', 'packages/cli', 'packages/utils')
}

Task InstallAll {
Write-Host "Installing all workspace dependencies..." -ForegroundColor Green
exec { npm install }
}

Task BuildAll {
Write-Host "Building all workspaces..." -ForegroundColor Green

foreach ($workspace in $Workspaces) {
Write-Host " Building $workspace..." -ForegroundColor Cyan
exec { npm run build --workspace=$workspace }
}
}

Task TestAll {
Write-Host "Testing all workspaces..." -ForegroundColor Green
exec { npm test --workspaces }
}

Task BuildWorkspace {
param([string]$Name)

if ([string]::IsNullOrEmpty($Name)) {
throw "Workspace name is required. Usage: Invoke-psake BuildWorkspace -parameters @{Name='packages/core'}"
}

Write-Host "Building workspace: $Name" -ForegroundColor Green
exec { npm run build --workspace=$Name }
}

Task PublishWorkspace {
param([string]$Name)

if ([string]::IsNullOrEmpty($Name)) {
throw "Workspace name is required"
}

Write-Host "Publishing workspace: $Name" -ForegroundColor Green
exec { npm publish --workspace=$Name --access public }
}

Docker Integration

Combine psake with Docker for containerized Node.js builds:

Properties {
$ImageName = 'myapp'
$ImageTag = 'latest'
$DockerRegistry = 'docker.io'
}

Task DockerBuild -depends Test {
Write-Host "Building Docker image..." -ForegroundColor Green

$fullImageName = "${DockerRegistry}/${ImageName}:${ImageTag}"
exec { docker build -t $fullImageName . }

Write-Host "Docker image built: $fullImageName" -ForegroundColor Green
}

Task DockerRun {
Write-Host "Running Docker container..." -ForegroundColor Green

$fullImageName = "${DockerRegistry}/${ImageName}:${ImageTag}"
exec { docker run -p 3000:3000 $fullImageName }
}

Task DockerPush -depends DockerBuild {
Write-Host "Pushing Docker image to registry..." -ForegroundColor Green

$fullImageName = "${DockerRegistry}/${ImageName}:${ImageTag}"
exec { docker push $fullImageName }
}

Example Dockerfile for Node.js:

FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM node:18-alpine

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

EXPOSE 3000

CMD ["node", "dist/index.js"]

CI/CD Integration

Example integration with CI/CD platforms:

GitHub Actions

name: Build

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'

- name: Install psake
shell: pwsh
run: Install-Module -Name psake -Scope CurrentUser -Force

- name: Run psake build
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1 -taskList Test

- name: Publish to npm
if: github.ref == 'refs/heads/main'
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1 -taskList PublishPackage
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Best Practices

1. Lock Dependencies

Always commit package-lock.json and use npm ci in CI/CD:

Task Install {
if ($env:CI -eq 'true') {
exec { npm ci } # Clean install from lockfile
} else {
exec { npm install } # Allow updates locally
}
}

2. Environment-Specific Builds

Use environment variables and different configurations:

Properties {
$Environment = if ($env:NODE_ENV) { $env:NODE_ENV } else { 'development' }
}

Task Build {
Write-Host "Building for environment: $Environment" -ForegroundColor Green

$env:NODE_ENV = $Environment
exec { npm run build }
}

3. Version Bumping

Automate version bumping:

Task BumpVersion {
param([string]$Type = 'patch')

Write-Host "Bumping $Type version..." -ForegroundColor Green
exec { npm version $Type --no-git-tag-version }

$pkg = Get-Content 'package.json' | ConvertFrom-Json
Write-Host "New version: $($pkg.version)" -ForegroundColor Green
}

4. Clean Node Modules

Periodically clean and reinstall:

Task CleanInstall {
Write-Host "Cleaning node_modules..." -ForegroundColor Green

if (Test-Path $NodeModules) {
Remove-Item $NodeModules -Recurse -Force
}

if (Test-Path 'package-lock.json') {
Remove-Item 'package-lock.json' -Force
}

exec { npm install }
}

Troubleshooting

npm Command Not Found

Problem: exec: npm: The term 'npm' is not recognized

Solution: Ensure Node.js is installed and in PATH:

Task VerifyNode {
try {
$nodeVersion = node --version
$npmVersion = npm --version
Write-Host "Node.js: $nodeVersion" -ForegroundColor Green
Write-Host "npm: $npmVersion" -ForegroundColor Green
}
catch {
throw "Node.js and npm are required. Install from https://nodejs.org/"
}
}

Task Build -depends VerifyNode {
exec { npm run build }
}

Module Not Found Errors

Problem: Build fails with "Cannot find module" errors

Solution: Ensure dependencies are installed:

Task Build -depends Install {
if (-not (Test-Path $NodeModules)) {
throw "node_modules not found. Run Install task first."
}

exec { npm run build }
}

Permission Errors on Linux/macOS

Problem: EACCES errors when installing global packages

Solution: Use --prefix or configure npm properly:

Task InstallGlobal {
$npmPrefix = if ($IsLinux -or $IsMacOS) {
"$HOME/.npm-global"
} else {
"$env:APPDATA\npm"
}

exec { npm config set prefix $npmPrefix }
exec { npm install -g typescript }
}

Build Fails Due to Memory Issues

Problem: JavaScript heap out of memory

Solution: Increase Node.js memory limit:

Task Build {
$env:NODE_OPTIONS = '--max-old-space-size=4096'
exec { npm run build }
}

TypeScript Compilation Errors

Problem: Type errors break the build

Solution: Add separate type-checking task:

Task TypeCheck {
Write-Host "Type checking..." -ForegroundColor Green
exec { npx tsc --noEmit }
}

Task Build -depends TypeCheck {
exec { npx tsc }
}

See Also