feat: add support ofr default value syntax

- Introduced a new script `test-default-values.js` to validate default value syntax in commands.
- Updated `package.json` to include the new test script in the e2e test command.
- Modified GitHub Actions workflow to run the new e2e tests.
- Enhanced `commandConvert` function to support default value syntax in environment variables.
- Added unit tests for the new functionality in `command-default-values.test.ts`.
This commit is contained in:
Kent C. Dodds
2025-09-29 11:32:08 -06:00
parent ce1e93646e
commit f9a8b87de4
5 changed files with 311 additions and 3 deletions
+3
View File
@@ -90,6 +90,9 @@ jobs:
- name: 🐚 Run cross-env-shell e2e tests
run: node e2e/test-cross-env-shell.js
- name: 🔧 Run default value syntax e2e tests
run: node e2e/test-default-values.js
release:
name: 🚀 Release
needs: [build, e2e]
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env node
import { spawn } from 'child_process'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// Path to the built cross-env binary
const crossEnvPath = join(__dirname, '..', 'dist', 'bin', 'cross-env.js')
function runCommand(command, args = []) {
return new Promise((resolve, reject) => {
const child = spawn('node', [command, ...args], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env },
})
let stdout = ''
let stderr = ''
child.stdout.on('data', (data) => {
stdout += data.toString()
})
child.stderr.on('data', (data) => {
stderr += data.toString()
})
child.on('close', (code) => {
if (code === 0) {
resolve({ stdout, stderr, code })
} else {
reject(new Error(`Command failed with code ${code}: ${stderr}`))
}
})
child.on('error', (error) => {
reject(error)
})
})
}
async function runTests() {
console.log('🧪 Running cross-env default value syntax e2e tests...')
const tests = [
{
name: 'Default value syntax when variable is undefined',
test: async () => {
// Test that the command conversion works by using echo to see the converted command
const result = await runCommand(crossEnvPath, [
'echo',
'wrangler dev --port ${PORT:-8787}',
])
// On Windows, this should convert to the default value
// On Unix, it should remain unchanged
const expected =
process.platform === 'win32'
? 'wrangler dev --port 8787'
: 'wrangler dev --port ${PORT:-8787}'
if (result.stdout.trim() !== expected) {
throw new Error(
`Expected '${expected}', got '${result.stdout.trim()}'`,
)
}
},
},
{
name: 'Default value syntax when variable is defined',
test: async () => {
const result = await runCommand(crossEnvPath, [
'PORT=3000',
'echo',
'wrangler dev --port ${PORT:-8787}',
])
// On Windows, this should convert to the defined value
// On Unix, it should remain unchanged
const expected =
process.platform === 'win32'
? 'wrangler dev --port 3000'
: 'wrangler dev --port ${PORT:-8787}'
if (result.stdout.trim() !== expected) {
throw new Error(
`Expected '${expected}', got '${result.stdout.trim()}'`,
)
}
},
},
{
name: 'Multiple default value syntaxes',
test: async () => {
const result = await runCommand(crossEnvPath, [
'HOST=localhost',
'echo',
'wrangler dev --host ${HOST:-0.0.0.0} --port ${PORT:-8787}',
])
// On Windows, should use defined value for HOST and default for PORT
// On Unix, should remain unchanged
const expected =
process.platform === 'win32'
? 'wrangler dev --host localhost --port 8787'
: 'wrangler dev --host ${HOST:-0.0.0.0} --port ${PORT:-8787}'
if (result.stdout.trim() !== expected) {
throw new Error(
`Expected '${expected}', got '${result.stdout.trim()}'`,
)
}
},
},
{
name: 'Default value with special characters',
test: async () => {
const result = await runCommand(crossEnvPath, [
'echo',
'node app.js --env ${NODE_ENV:-development}',
])
// On Windows, should use the default value
// On Unix, should remain unchanged
const expected =
process.platform === 'win32'
? 'node app.js --env development'
: 'node app.js --env ${NODE_ENV:-development}'
if (result.stdout.trim() !== expected) {
throw new Error(
`Expected '${expected}', got '${result.stdout.trim()}'`,
)
}
},
},
{
name: 'Default value with spaces',
test: async () => {
const result = await runCommand(crossEnvPath, [
'echo',
'echo ${MESSAGE:-hello world}',
])
// On Windows, should use the default value with spaces
// On Unix, should remain unchanged
const expected =
process.platform === 'win32'
? 'echo hello world'
: 'echo ${MESSAGE:-hello world}'
if (result.stdout.trim() !== expected) {
throw new Error(
`Expected '${expected}', got '${result.stdout.trim()}'`,
)
}
},
},
]
let passed = 0
let failed = 0
for (const test of tests) {
try {
console.log(` ✓ ${test.name}`)
await test.test()
passed++
} catch (error) {
console.log(` ✗ ${test.name}: ${error.message}`)
failed++
}
}
console.log(`\n📊 Test Results: ${passed} passed, ${failed} failed`)
if (failed > 0) {
process.exit(1)
}
console.log('✅ All default value syntax e2e tests passed!')
}
runTests().catch((error) => {
console.error('❌ Test runner failed:', error)
process.exit(1)
})
+1 -1
View File
@@ -24,7 +24,7 @@
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e": "node e2e/test-cross-env.js && node e2e/test-cross-env-shell.js",
"test:e2e": "node e2e/test-cross-env.js && node e2e/test-cross-env-shell.js && node e2e/test-default-values.js",
"validate": "npm run build && npm run typecheck && npm run lint && npm run format:check && npm run test:run"
},
"files": [
@@ -0,0 +1,103 @@
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
import { commandConvert } from '../command.js'
import * as isWindowsModule from '../is-windows.js'
vi.mock('../is-windows.js')
describe('commandConvert - Environment Variable Default Values', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllMocks()
})
test('should handle ${VAR:-default} syntax on Windows when VAR is undefined', () => {
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
const env = {} // PORT is not defined
const command = 'wrangler dev --port ${PORT:-8787}'
const result = commandConvert(command, env)
// Should use the default value when PORT is not defined
expect(result).toBe('wrangler dev --port 8787')
})
test('should handle ${VAR:-default} syntax on Windows when VAR is defined', () => {
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
const env = { PORT: '3000' }
const command = 'wrangler dev --port ${PORT:-8787}'
const result = commandConvert(command, env)
// Should use the defined value when PORT is defined
expect(result).toBe('wrangler dev --port 3000')
})
test('should handle ${VAR:-default} syntax on Unix (no conversion needed)', () => {
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
const env = {} // PORT is not defined
const command = 'wrangler dev --port ${PORT:-8787}'
const result = commandConvert(command, env)
// On Unix, no conversion is needed
expect(result).toBe('wrangler dev --port ${PORT:-8787}')
})
test('should handle simple ${VAR} syntax on Windows', () => {
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
const env = { PORT: '3000' }
const command = 'wrangler dev --port ${PORT}'
const result = commandConvert(command, env)
// This works correctly
expect(result).toBe('wrangler dev --port %PORT%')
})
test('should handle simple ${VAR} syntax on Windows when VAR is undefined', () => {
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
const env = {} // PORT is not defined
const command = 'wrangler dev --port ${PORT}'
const result = commandConvert(command, env)
// This works correctly - strips undefined variables
expect(result).toBe('wrangler dev --port ')
})
test('should handle multiple default value syntaxes in one command', () => {
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
const env = { HOST: 'localhost' } // PORT is not defined
const command = 'wrangler dev --host ${HOST:-0.0.0.0} --port ${PORT:-8787}'
const result = commandConvert(command, env)
// Should use defined value for HOST and default for PORT
expect(result).toBe('wrangler dev --host localhost --port 8787')
})
test('should handle default values with special characters', () => {
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
const env = {} // NODE_ENV is not defined
const command = 'node app.js --env ${NODE_ENV:-development}'
const result = commandConvert(command, env)
// Should use the default value
expect(result).toBe('node app.js --env development')
})
test('should handle default values with spaces', () => {
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
const env = {} // MESSAGE is not defined
const command = 'echo ${MESSAGE:-hello world}'
const result = commandConvert(command, env)
// Should use the default value with spaces
expect(result).toBe('echo hello world')
})
})
+19 -2
View File
@@ -17,8 +17,25 @@ export function commandConvert(
return command
}
const envUnixRegex = /\$(\w+)|\${(\w+)}/g // $my_var or ${my_var}
const convertedCmd = command.replace(envUnixRegex, (match, $1, $2) => {
// Handle simple variables: $var or ${var}
const simpleEnvRegex = /\$(\w+)|\${(\w+)}/g
// Handle bash parameter expansion with default values: ${var:-default}
const defaultValueRegex = /\$\{(\w+):-([^}]+)\}/g
let convertedCmd = command
// First, handle bash parameter expansion with default values
convertedCmd = convertedCmd.replace(
defaultValueRegex,
(match, varName, defaultValue) => {
// If the variable exists, use its value; otherwise use the default
const value = env[varName] || defaultValue
return value
},
)
// Then handle simple variable references
convertedCmd = convertedCmd.replace(simpleEnvRegex, (match, $1, $2) => {
const varName = $1 || $2
// In Windows, non-existent variables are not replaced by the shell,
// so for example "echo %FOO%" will literally print the string "%FOO%", as