mirror of
https://github.com/hanzoai/cross-env.git
synced 2026-09-24 16:58:15 +00:00
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:
@@ -90,6 +90,9 @@ jobs:
|
|||||||
- name: 🐚 Run cross-env-shell e2e tests
|
- name: 🐚 Run cross-env-shell e2e tests
|
||||||
run: node e2e/test-cross-env-shell.js
|
run: node e2e/test-cross-env-shell.js
|
||||||
|
|
||||||
|
- name: 🔧 Run default value syntax e2e tests
|
||||||
|
run: node e2e/test-default-values.js
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: 🚀 Release
|
name: 🚀 Release
|
||||||
needs: [build, e2e]
|
needs: [build, e2e]
|
||||||
|
|||||||
@@ -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
@@ -24,7 +24,7 @@
|
|||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"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"
|
"validate": "npm run build && npm run typecheck && npm run lint && npm run format:check && npm run test:run"
|
||||||
},
|
},
|
||||||
"files": [
|
"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
@@ -17,8 +17,25 @@ export function commandConvert(
|
|||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
const envUnixRegex = /\$(\w+)|\${(\w+)}/g // $my_var or ${my_var}
|
// Handle simple variables: $var or ${var}
|
||||||
const convertedCmd = command.replace(envUnixRegex, (match, $1, $2) => {
|
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
|
const varName = $1 || $2
|
||||||
// In Windows, non-existent variables are not replaced by the shell,
|
// In Windows, non-existent variables are not replaced by the shell,
|
||||||
// so for example "echo %FOO%" will literally print the string "%FOO%", as
|
// so for example "echo %FOO%" will literally print the string "%FOO%", as
|
||||||
|
|||||||
Reference in New Issue
Block a user