mirror of
https://github.com/hanzoai/cross-env.git
synced 2026-09-24 16:58:15 +00:00
feat: modernize cross-env with TypeScript, Vitest, and ESM-only build (#261)
NOTE: For the vast majority of users running supported versions of Node.js, this should not require any changes and you can update without issue. BREAKING CHANGE: This is a major rewrite that changes the module format from CommonJS to ESM-only. The package now requires Node.js >=20 and only exports ESM modules (not relevant in most cases). BREAKING CHANGE: The package.json exports have been updated to remove CommonJS support. Only ESM imports are now supported. BREAKING CHANGE: All source files have been converted from JavaScript to TypeScript, requiring TypeScript-aware tooling for development. - Replace Jest with Vitest for testing - Convert all source files from .js to .ts with proper TypeScript types - Use zshy for ESM-only builds (removes CJS support) - Adopt @epic-web/config for TypeScript, ESLint, and Prettier - Update to Node.js >=20 requirement - Remove kcd-scripts dependency - Add comprehensive e2e tests with GitHub Actions matrix testing - Update GitHub workflow with caching and cross-platform testing - Modernize documentation and remove outdated sections - Update all dependencies to latest versions - Add proper TypeScript declarations and exports The tool maintains its original functionality while being completely modernized with the latest tooling and best practices Co-authored-by: GitHub Action <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
GitHub Action
parent
79a99c6d32
commit
0112288e01
@@ -1,255 +0,0 @@
|
||||
{
|
||||
"projectName": "cross-env",
|
||||
"projectOwner": "kentcdodds",
|
||||
"repoType": "github",
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "kentcdodds",
|
||||
"name": "Kent C. Dodds",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3",
|
||||
"profile": "https://kentcdodds.com",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"infra",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zhuangya",
|
||||
"name": "Ya Zhuang ",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/499038?v=3",
|
||||
"profile": "https://zhuangya.me",
|
||||
"contributions": [
|
||||
"plugin",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wopian",
|
||||
"name": "James Harris",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/3440094?v=3",
|
||||
"profile": "https://wopian.me",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "compumike08",
|
||||
"name": "compumike08",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/8941730?v=3",
|
||||
"profile": "https://github.com/compumike08",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"doc",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "danielo515",
|
||||
"name": "Daniel Rodríguez Rivero",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/2270425?v=3",
|
||||
"profile": "https://github.com/danielo515",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "inyono",
|
||||
"name": "Jonas Keinholz",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/1508477?v=3",
|
||||
"profile": "https://github.com/inyono",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hgwood",
|
||||
"name": "Hugo Wood",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/1656170?v=3",
|
||||
"profile": "https://github.com/hgwood",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "thomasthiebaud",
|
||||
"name": "Thiebaud Thomas",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/3715715?v=3",
|
||||
"profile": "https://github.com/thomasthiebaud",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "DanReyLop",
|
||||
"name": "Daniel Rey López",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1715800?v=3",
|
||||
"profile": "https://daniel.blog",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "amilajack",
|
||||
"name": "Amila Welihinda",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/6374832?v=3",
|
||||
"profile": "http://amilajack.com",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "paulcbetts",
|
||||
"name": "Paul Betts",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1396?v=3",
|
||||
"profile": "https://twitter.com/paulcbetts",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "turnerhayes",
|
||||
"name": "Turner Hayes",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/6371670?v=3",
|
||||
"profile": "https://github.com/turnerhayes",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sudo-suhas",
|
||||
"name": "Suhas Karanth",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/22251956?v=4",
|
||||
"profile": "https://github.com/sudo-suhas",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sventschui",
|
||||
"name": "Sven",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/512692?v=4",
|
||||
"profile": "https://github.com/sventschui",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"example",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "NicoZelaya",
|
||||
"name": "D. Nicolás Lopez Zelaya",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/5522668?v=4",
|
||||
"profile": "https://github.com/NicoZelaya",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bithavoc",
|
||||
"name": "Johan Hernandez",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/219289?v=4",
|
||||
"profile": "http://bithavoc.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jnielson94",
|
||||
"name": "Jordan Nielson",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/13559161?v=4",
|
||||
"profile": "https://github.com/jnielson94",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Jason-Cooke",
|
||||
"name": "Jason Cooke",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/5185660?v=4",
|
||||
"profile": "https://nz.linkedin.com/in/jsonc11",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bibo5088",
|
||||
"name": "bibo5088",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/17709887?v=4",
|
||||
"profile": "https://github.com/bibo5088",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "coderberry",
|
||||
"name": "Eric Berry",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/12481?v=4",
|
||||
"profile": "https://codefund.io",
|
||||
"contributions": [
|
||||
"fundingFinding"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MichaelDeBoey",
|
||||
"name": "Michaël De Boey",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4",
|
||||
"profile": "https://michaeldeboey.be",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lauriii",
|
||||
"name": "Lauri Eskola",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1845495?v=4",
|
||||
"profile": "https://github.com/lauriii",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "devuxer",
|
||||
"name": "devuxer",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1298521?v=4",
|
||||
"profile": "https://github.com/devuxer",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dsbert",
|
||||
"name": "Daniel",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/1320090?v=4",
|
||||
"profile": "https://github.com/dsbert",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"repoHost": "https://github.com",
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true
|
||||
}
|
||||
@@ -18,6 +18,7 @@ learn how: http://kcd.im/pull-request
|
||||
Relevant code or config
|
||||
|
||||
```javascript
|
||||
|
||||
```
|
||||
|
||||
What you did:
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
name: Auto Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
format:
|
||||
name: 🔧 Auto Format
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check current formatting
|
||||
id: format-check
|
||||
run: |
|
||||
if npm run format:check; then
|
||||
echo "format_needed=false" >> $GITHUB_OUTPUT
|
||||
echo "✅ Code is already properly formatted"
|
||||
else
|
||||
echo "format_needed=true" >> $GITHUB_OUTPUT
|
||||
echo "⚠️ Code formatting issues found"
|
||||
fi
|
||||
|
||||
- name: Format code
|
||||
if: steps.format-check.outputs.format_needed == 'true'
|
||||
run: npm run format
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
if: steps.format-check.outputs.format_needed == 'true'
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "ℹ️ No formatting changes to commit"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "📝 Formatting changes detected"
|
||||
fi
|
||||
|
||||
# CI cannot commit to workflow files, so we need to check for non-workflow changes
|
||||
- name: Check for non-workflow changes
|
||||
id: non-workflow-changes
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git add -A
|
||||
git reset .github/workflows/
|
||||
if git diff --cached --quiet; then
|
||||
echo "has_non_workflow_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "ℹ️ Only workflow files have formatting changes - skipping commit"
|
||||
else
|
||||
echo "has_non_workflow_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "📝 Non-workflow formatting changes detected"
|
||||
fi
|
||||
|
||||
- name: Configure Git
|
||||
if:
|
||||
steps.non-workflow-changes.outputs.has_non_workflow_changes == 'true'
|
||||
run: |
|
||||
git config --local user.email "[email protected]"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
- name: Commit and push changes
|
||||
if:
|
||||
steps.non-workflow-changes.outputs.has_non_workflow_changes == 'true'
|
||||
env:
|
||||
TARGET_REF: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
git commit -m "chore: 🔧 Auto-format code with Prettier [skip ci]
|
||||
|
||||
This commit was automatically generated by the auto-format workflow.
|
||||
Changes include:
|
||||
- Code formatting fixes
|
||||
- Consistent indentation
|
||||
- Proper line endings"
|
||||
git push origin HEAD:"$TARGET_REF"
|
||||
|
||||
- name: Comment on PR
|
||||
if:
|
||||
steps.non-workflow-changes.outputs.has_non_workflow_changes == 'true'
|
||||
&& github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '🔧 **Auto-formatting applied!**\n\nI\'ve automatically formatted the code using Prettier and pushed the changes. The formatting is now consistent with the project standards.'
|
||||
})
|
||||
|
||||
- name: Success message
|
||||
if:
|
||||
steps.changes.outputs.has_changes == 'false' ||
|
||||
steps.format-check.outputs.format_needed == 'false' ||
|
||||
steps.non-workflow-changes.outputs.has_non_workflow_changes == 'false'
|
||||
run: |
|
||||
if [ "${{ steps.changes.outputs.has_changes }}" == "true" ] && [ "${{ steps.non-workflow-changes.outputs.has_non_workflow_changes }}" == "false" ]; then
|
||||
echo "✅ Only workflow files had formatting changes - no commit needed!"
|
||||
else
|
||||
echo "✅ No formatting changes needed - code is already properly formatted!"
|
||||
fi
|
||||
@@ -1,88 +1,84 @@
|
||||
name: validate
|
||||
name: Validate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '+([0-9])?(.{+([0-9]),x}).x'
|
||||
- 'master'
|
||||
- 'next'
|
||||
- 'next-major'
|
||||
- 'beta'
|
||||
- 'alpha'
|
||||
- '!all-contributors/**'
|
||||
pull_request: {}
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
# ignore all-contributors PRs
|
||||
if: ${{ !contains(github.head_ref, 'all-contributors') }}
|
||||
build:
|
||||
name: 🔨 Build and Validate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22, 24]
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🟢 Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: 🔨 Build
|
||||
run: npm run build
|
||||
|
||||
- name: 🔍 Type check
|
||||
run: npm run typecheck
|
||||
|
||||
- name: 🧹 Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: 🧪 Test
|
||||
run: npm run test:run
|
||||
|
||||
- name: 📊 Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
e2e:
|
||||
name: 🌐 E2E Tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
node: [10.14, 10, 12, 14, 15]
|
||||
runs-on: ${{ matrix.os }}
|
||||
node-version: [24]
|
||||
|
||||
steps:
|
||||
- name: 🛑 Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
- name: 📥 Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🟢 Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
- name: 📦 Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: 🔨 Build
|
||||
run: npm run build
|
||||
|
||||
- name: 📥 Download deps
|
||||
uses: bahmutov/npm-install@v1
|
||||
with:
|
||||
useLockFile: false
|
||||
# ensure we don't implicitly rely on dev dependencies
|
||||
- name: 📦 Prune dependencies
|
||||
run: npm prune --omit=dev
|
||||
|
||||
- name: ▶️ Run validate script
|
||||
run: npm run validate
|
||||
- name: 🧪 Run cross-env e2e tests
|
||||
run: node e2e/test-cross-env.js
|
||||
|
||||
- name: ⬆️ Upload coverage report
|
||||
uses: codecov/codecov-action@v1
|
||||
|
||||
release:
|
||||
needs: main
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
${{ github.repository == 'kentcdodds/cross-env' &&
|
||||
contains('refs/heads/master,refs/heads/beta,refs/heads/next,refs/heads/alpha',
|
||||
github.ref) && github.event_name == 'push' }}
|
||||
steps:
|
||||
- name: 🛑 Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
- name: 📥 Download deps
|
||||
uses: bahmutov/npm-install@v1
|
||||
with:
|
||||
useLockFile: false
|
||||
|
||||
- name: 🚀 Release
|
||||
uses: cycjimmy/semantic-release-action@v2
|
||||
with:
|
||||
semantic_version: 17
|
||||
branches: |
|
||||
[
|
||||
'+([0-9])?(.{+([0-9]),x}).x',
|
||||
'master',
|
||||
'next',
|
||||
'next-major',
|
||||
{name: 'beta', prerelease: true},
|
||||
{name: 'alpha', prerelease: true}
|
||||
]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: 🐚 Run cross-env-shell e2e tests
|
||||
run: node e2e/test-cross-env-shell.js
|
||||
|
||||
@@ -3,7 +3,3 @@ coverage
|
||||
dist
|
||||
.opt-in
|
||||
.opt-out
|
||||
|
||||
# Lock files
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('kcd-scripts/husky')
|
||||
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
coverage
|
||||
dist
|
||||
package-lock.json
|
||||
|
||||
+84
-9
@@ -12,20 +12,44 @@ series [How to Contribute to an Open Source Project on GitHub][egghead]
|
||||
3. `npm run validate` to validate you've got it working
|
||||
4. Create a branch for your PR
|
||||
|
||||
> Tip: Keep your `master` branch pointing at the original repository and make
|
||||
> pull requests from branches on your fork. To do this, run:
|
||||
> Tip: Keep your `main` branch pointing at the original repository and make pull
|
||||
> requests from branches on your fork. To do this, run:
|
||||
>
|
||||
> ```
|
||||
> git remote add upstream https://github.com/kentcdodds/cross-env.git
|
||||
> git fetch upstream
|
||||
> git branch --set-upstream-to=upstream/master master
|
||||
> git branch --set-upstream-to=upstream/main main
|
||||
> ```
|
||||
>
|
||||
> This will add the original repository as a "remote" called "upstream," Then
|
||||
> fetch the git information from that remote, then set your local `master`
|
||||
> branch to use the upstream master branch whenever you run `git pull`. Then you
|
||||
> can make all of your pull request branches based on this `master` branch.
|
||||
> Whenever you want to update your version of `master`, do a regular `git pull`.
|
||||
> fetch the git information from that remote, then set your local `main` branch
|
||||
> to use the upstream main branch whenever you run `git pull`. Then you can make
|
||||
> all of your pull request branches based on this `main` branch. Whenever you
|
||||
> want to update your version of `main`, do a regular `git pull`.
|
||||
|
||||
## Development
|
||||
|
||||
This project uses modern tooling:
|
||||
|
||||
- **TypeScript** for type safety
|
||||
- **Vitest** for testing
|
||||
- **ESLint** for linting
|
||||
- **Prettier** for formatting
|
||||
- **zshy** for building
|
||||
|
||||
### Available Scripts
|
||||
|
||||
- `npm run build` - Build the project
|
||||
- `npm run dev` - Build in watch mode
|
||||
- `npm run test` - Run tests in watch mode
|
||||
- `npm run test:run` - Run tests once
|
||||
- `npm run test:coverage` - Run tests with coverage
|
||||
- `npm run lint` - Run ESLint
|
||||
- `npm run lint:fix` - Fix ESLint issues
|
||||
- `npm run format` - Format code with Prettier
|
||||
- `npm run format:check` - Check code formatting
|
||||
- `npm run typecheck` - Run TypeScript type checking
|
||||
- `npm run validate` - Run all checks (build, typecheck, lint, format, test)
|
||||
|
||||
## Help needed
|
||||
|
||||
@@ -34,7 +58,58 @@ Please checkout the [the open issues][issues]
|
||||
Also, please watch the repo and respond to questions/bug reports/feature
|
||||
requests! Thanks!
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these people ([emoji key][emojis]):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://kentcdodds.com"><img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;" alt=""/><br /><sub><b>Kent C. Dodds</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=kentcdodds" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=kentcdodds" title="Documentation">📖</a> <a href="#infra-kentcdodds" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=kentcdodds" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://zhuangya.me"><img src="https://avatars1.githubusercontent.com/u/499038?v=3" width="100px;" alt=""/><br /><sub><b>Ya Zhuang </b></sub></a><br /><a href="#plugin-zhuangya" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=zhuangya" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://wopian.me"><img src="https://avatars3.githubusercontent.com/u/3440094?v=3" width="100px;" alt=""/><br /><sub><b>James Harris</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=wopian" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/compumike08"><img src="https://avatars1.githubusercontent.com/u/8941730?v=3" width="100px;" alt=""/><br /><sub><b>compumike08</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Acompumike08" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=compumike08" title="Documentation">📖</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=compumike08" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/danielo515"><img src="https://avatars1.githubusercontent.com/u/2270425?v=3" width="100px;" alt=""/><br /><sub><b>Daniel Rodríguez Rivero</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Adanielo515" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=danielo515" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=danielo515" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/inyono"><img src="https://avatars2.githubusercontent.com/u/1508477?v=3" width="100px;" alt=""/><br /><sub><b>Jonas Keinholz</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Ainyono" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=inyono" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=inyono" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/hgwood"><img src="https://avatars3.githubusercontent.com/u/1656170?v=3" width="100px;" alt=""/><br /><sub><b>Hugo Wood</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Ahgwood" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=hgwood" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=hgwood" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/thomasthiebaud"><img src="https://avatars0.githubusercontent.com/u/3715715?v=3" width="100px;" alt=""/><br /><sub><b>Thiebaud Thomas</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Athomasthiebaud" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=thomasthiebaud" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=thomasthiebaud" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://daniel.blog"><img src="https://avatars1.githubusercontent.com/u/1715800?v=3" width="100px;" alt=""/><br /><sub><b>Daniel Rey López</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=DanReyLop" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=DanReyLop" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="http://amilajack.com"><img src="https://avatars2.githubusercontent.com/u/6374832?v=3" width="100px;" alt=""/><br /><sub><b>Amila Welihinda</b></sub></a><br /><a href="#infra-amilajack" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://twitter.com/paulcbetts"><img src="https://avatars1.githubusercontent.com/u/1396?v=3" width="100px;" alt=""/><br /><sub><b>Paul Betts</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Apaulcbetts" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=paulcbetts" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/turnerhayes"><img src="https://avatars1.githubusercontent.com/u/6371670?v=3" width="100px;" alt=""/><br /><sub><b>Turner Hayes</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Aturnerhayes" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=turnerhayes" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=turnerhayes" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/sudo-suhas"><img src="https://avatars2.githubusercontent.com/u/22251956?v=4" width="100px;" alt=""/><br /><sub><b>Suhas Karanth</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=sudo-suhas" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=sudo-suhas" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/sventschui"><img src="https://avatars3.githubusercontent.com/u/512692?v=4" width="100px;" alt=""/><br /><sub><b>Sven</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=sventschui" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=sventschui" title="Documentation">📖</a> <a href="#example-sventschui" title="Examples">💡</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=sventschui" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/NicoZelaya"><img src="https://avatars0.githubusercontent.com/u/5522668?v=4" width="100px;" alt=""/><br /><sub><b>D. Nicolás Lopez Zelaya</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=NicoZelaya" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://bithavoc.io"><img src="https://avatars3.githubusercontent.com/u/219289?v=4" width="100px;" alt=""/><br /><sub><b>Johan Hernandez</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=bithavoc" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jnielson94"><img src="https://avatars3.githubusercontent.com/u/13559161?v=4" width="100px;" alt=""/><br /><sub><b>Jordan Nielson</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Ajnielson94" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=jnielson94" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=jnielson94" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://nz.linkedin.com/in/jsonc11"><img src="https://avatars0.githubusercontent.com/u/5185660?v=4" width="100px;" alt=""/><br /><sub><b>Jason Cooke</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=Jason-Cooke" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/bibo5088"><img src="https://avatars0.githubusercontent.com/u/17709887?v=4" width="100px;" alt=""/><br /><sub><b>bibo5088</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=bibo5088" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://codefund.io"><img src="https://avatars2.githubusercontent.com/u/12481?v=4" width="100px;" alt=""/><br /><sub><b>Eric Berry</b></sub></a><br /><a href="#fundingFinding-coderberry" title="Funding Finding">🔍</a></td>
|
||||
<td align="center"><a href="https://michaeldeboey.be"><img src="https://avatars3.githubusercontent.com/u/6643991?v=4" width="100px;" alt=""/><br /><sub><b>Michaël De Boey</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=MichaelDeBoey" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/lauriii"><img src="https://avatars0.githubusercontent.com/u/1845495?v=4" width="100px;" alt=""/><br /><sub><b>Lauri Eskola</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=lauriii" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/devuxer"><img src="https://avatars0.githubusercontent.com/u/1298521?v=4" width="100px;" alt=""/><br /><sub><b>devuxer</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=devuxer" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/dsbert"><img src="https://avatars2.githubusercontent.com/u/1320090?v=4" width="100px;" alt=""/><br /><sub><b>Daniel</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=dsbert" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors][all-contributors] specification.
|
||||
Contributions of any kind welcome!
|
||||
|
||||
[egghead]:
|
||||
https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
|
||||
[all-contributors]: https://github.com/kentcdodds/all-contributors
|
||||
https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
|
||||
[all-contributors]: https://github.com/all-contributors/all-contributors
|
||||
[emojis]: https://allcontributors.org/docs/en/emoji-key
|
||||
[issues]: https://github.com/kentcdodds/cross-env/issues
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2017 Kent C. Dodds
|
||||
Copyright (c) 2017-2025 Kent C. Dodds
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,82 +1,36 @@
|
||||
<div align="center">
|
||||
<h1>cross-env 🔀</h1>
|
||||
# cross-env 🔀
|
||||
|
||||
<p>Run scripts that set and use environment variables across platforms</p>
|
||||
</div>
|
||||
Run scripts that set and use environment variables across platforms.
|
||||
|
||||
**🚨 NOTICE: cross-env still works well, but is in maintenance mode. No new
|
||||
features will be added, only serious and common-case bugs will be fixed, and it
|
||||
will only be kept up-to-date with Node.js over time.
|
||||
[Learn more](https://github.com/kentcdodds/cross-env/issues/257)**
|
||||
|
||||
---
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[![Build Status][build-badge]][build]
|
||||
[![Code Coverage][coverage-badge]][coverage]
|
||||
[![version][version-badge]][package]
|
||||
[![downloads][downloads-badge]][npmtrends]
|
||||
[![MIT License][license-badge]][license]
|
||||
[![All Contributors][all-contributors-badge]](#contributors-)
|
||||
[![Code of Conduct][coc-badge]][coc]
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## The problem
|
||||
## The Problem
|
||||
|
||||
Most Windows command prompts will choke when you set environment variables with
|
||||
`NODE_ENV=production` like that. (The exception is [Bash on Windows][win-bash],
|
||||
which uses native Bash.) Similarly, there's a difference in how windows and
|
||||
POSIX commands utilize environment variables. With POSIX, you use: `$ENV_VAR`
|
||||
and on windows you use `%ENV_VAR%`.
|
||||
`NODE_ENV=production` like that. Similarly, there's a difference in how Windows
|
||||
and POSIX commands utilize environment variables. With POSIX, you use:
|
||||
`$ENV_VAR` and on Windows you use `%ENV_VAR%`.
|
||||
|
||||
## This solution
|
||||
## The Solution
|
||||
|
||||
`cross-env` makes it so you can have a single command without worrying about
|
||||
setting or using the environment variable properly for the platform. Just set it
|
||||
like you would if it's running on a POSIX system, and `cross-env` will take care
|
||||
of setting it properly.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [`cross-env` vs `cross-env-shell`](#cross-env-vs-cross-env-shell)
|
||||
- [Windows Issues](#windows-issues)
|
||||
- [Inspiration](#inspiration)
|
||||
- [Other Solutions](#other-solutions)
|
||||
- [Issues](#issues)
|
||||
- [🐛 Bugs](#-bugs)
|
||||
- [💡 Feature Requests](#-feature-requests)
|
||||
- [Contributors ✨](#contributors-)
|
||||
- [LICENSE](#license)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Installation
|
||||
|
||||
This module is distributed via [npm][npm] which is bundled with [node][node] and
|
||||
should be installed as one of your project's `devDependencies`:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install --save-dev cross-env
|
||||
```
|
||||
|
||||
> WARNING! Make sure that when you're installing packages that you spell things
|
||||
> correctly to avoid [mistakenly installing malware][malware]
|
||||
|
||||
> NOTE : Version 7 of cross-env only supports Node.js 10 and higher, to use it
|
||||
> on Node.js 8 or lower install version 6 `npm install --save-dev cross-env@6`
|
||||
|
||||
## Usage
|
||||
|
||||
I use this in my npm scripts:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
|
||||
}
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production node ./start.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -84,211 +38,89 @@ Ultimately, the command that is executed (using [`cross-spawn`][cross-spawn])
|
||||
is:
|
||||
|
||||
```
|
||||
webpack --config build/webpack.config.js
|
||||
node ./start.js
|
||||
```
|
||||
|
||||
The `NODE_ENV` environment variable will be set by `cross-env`
|
||||
The `NODE_ENV` environment variable will be set by `cross-env`.
|
||||
|
||||
### Multiple Environment Variables
|
||||
|
||||
You can set multiple environment variables at a time:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "cross-env FIRST_ENV=one SECOND_ENV=two node ./my-program"
|
||||
}
|
||||
"scripts": {
|
||||
"build": "cross-env FIRST_ENV=one SECOND_ENV=two node ./my-program"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Commands
|
||||
|
||||
You can also split a command into several ones, or separate the environment
|
||||
variables declaration from the actual command execution. You can do it this way:
|
||||
variables declaration from the actual command execution:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"parentScript": "cross-env GREET=\"Joe\" npm run childScript",
|
||||
"childScript": "cross-env-shell \"echo Hello $GREET\""
|
||||
}
|
||||
"scripts": {
|
||||
"parentScript": "cross-env GREET=\"Joe\" npm run childScript",
|
||||
"childScript": "cross-env-shell \"echo Hello $GREET\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Where `childScript` holds the actual command to execute and `parentScript` sets
|
||||
the environment variables to use. Then instead of run the childScript you run
|
||||
the parent. This is quite useful for launching the same command with different
|
||||
env variables or when the environment variables are too long to have everything
|
||||
in one line. It also means that you can use `$GREET` env var syntax even on
|
||||
Windows which would usually require it to be `%GREET%`.
|
||||
### JSON Strings
|
||||
|
||||
If you precede a dollar sign with an odd number of backslashes the expression
|
||||
statement will not be replaced. Note that this means backslashes after the JSON
|
||||
string escaping took place. `"FOO=\\$BAR"` will not be replaced.
|
||||
`"FOO=\\\\$BAR"` will be replaced though.
|
||||
|
||||
Lastly, if you want to pass a JSON string (e.g., when using [ts-loader]), you
|
||||
can do as follows:
|
||||
For JSON strings (e.g., when using [ts-loader]):
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} node some_file.test.ts"
|
||||
}
|
||||
"scripts": {
|
||||
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} node some_file.test.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pay special attention to the **triple backslash** `(\\\)` **before** the
|
||||
**double quotes** `(")` and the **absence** of **single quotes** `(')`. Both of
|
||||
these conditions have to be met in order to work both on Windows and UNIX.
|
||||
**double quotes** `(")` and the **absence** of **single quotes** `(')`.
|
||||
|
||||
## `cross-env` vs `cross-env-shell`
|
||||
|
||||
The `cross-env` module exposes two bins: `cross-env` and `cross-env-shell`. The
|
||||
first one executes commands using [`cross-spawn`][cross-spawn], while the second
|
||||
one uses the `shell` option from Node's `spawn`.
|
||||
The `cross-env` module exposes two bins: `cross-env` and `cross-env-shell`.
|
||||
|
||||
The main use case for `cross-env-shell` is when you need an environment variable
|
||||
to be set across an entire inline shell script, rather than just one command.
|
||||
- **`cross-env`**: Executes commands using [`cross-spawn`][cross-spawn]
|
||||
- **`cross-env-shell`**: Uses the `shell` option from Node's `spawn`
|
||||
|
||||
For example, if you want to have the environment variable apply to several
|
||||
commands in series then you will need to wrap those in quotes and use
|
||||
`cross-env-shell` instead of `cross-env`.
|
||||
Use `cross-env-shell` when you need an environment variable to be set across an
|
||||
entire inline shell script, rather than just one command:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"greet": "cross-env-shell GREETING=Hi NAME=Joe \"echo $GREETING && echo $NAME\""
|
||||
}
|
||||
"scripts": {
|
||||
"greet": "cross-env-shell GREETING=Hi NAME=Joe \"echo $GREETING && echo $NAME\""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The rule of thumb is: if you want to pass to `cross-env` a command that contains
|
||||
**Rule of thumb**: If you want to pass to `cross-env` a command that contains
|
||||
special shell characters _that you want interpreted_, then use
|
||||
`cross-env-shell`. Otherwise stick to `cross-env`.
|
||||
|
||||
On Windows you need to use `cross-env-shell`, if you want to handle
|
||||
[signal events](https://nodejs.org/api/process.html#process_signal_events)
|
||||
inside of your program. A common case for that is when you want to capture a
|
||||
`SIGINT` event invoked by pressing `Ctrl + C` on the command-line interface.
|
||||
|
||||
## Windows Issues
|
||||
|
||||
Please note that `npm` uses `cmd` by default and that doesn't support command
|
||||
substitution, so if you want to leverage that, then you need to update your
|
||||
`.npmrc` to set the `script-shell` to powershell.
|
||||
[Learn more here](https://github.com/kentcdodds/cross-env/issues/192#issuecomment-513341729).
|
||||
|
||||
## Inspiration
|
||||
## Requirements
|
||||
|
||||
I originally created this to solve a problem I was having with my npm scripts in
|
||||
[angular-formly][angular-formly]. This made contributing to the project much
|
||||
easier for Windows users.
|
||||
- Node.js >= 20
|
||||
|
||||
## Other Solutions
|
||||
|
||||
- [`env-cmd`](https://github.com/toddbluhm/env-cmd) - Reads environment
|
||||
variables from a file instead
|
||||
- [`@naholyr/cross-env`](https://www.npmjs.com/package/@naholyr/cross-env) -
|
||||
`cross-env` with support for setting default values
|
||||
|
||||
## Issues
|
||||
|
||||
_Looking to contribute? Look for the [Good First Issue][good-first-issue]
|
||||
label._
|
||||
|
||||
### 🐛 Bugs
|
||||
|
||||
Please file an issue for bugs, missing documentation, or unexpected behavior.
|
||||
|
||||
[**See Bugs**][bugs]
|
||||
|
||||
### 💡 Feature Requests
|
||||
|
||||
This project is in maintenance mode and no new feature requests will be
|
||||
considered.
|
||||
|
||||
[**Learn more**](https://github.com/kentcdodds/cross-env/issues/257)
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these people ([emoji key][emojis]):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://kentcdodds.com"><img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;" alt=""/><br /><sub><b>Kent C. Dodds</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=kentcdodds" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=kentcdodds" title="Documentation">📖</a> <a href="#infra-kentcdodds" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=kentcdodds" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://zhuangya.me"><img src="https://avatars1.githubusercontent.com/u/499038?v=3" width="100px;" alt=""/><br /><sub><b>Ya Zhuang </b></sub></a><br /><a href="#plugin-zhuangya" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=zhuangya" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://wopian.me"><img src="https://avatars3.githubusercontent.com/u/3440094?v=3" width="100px;" alt=""/><br /><sub><b>James Harris</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=wopian" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/compumike08"><img src="https://avatars1.githubusercontent.com/u/8941730?v=3" width="100px;" alt=""/><br /><sub><b>compumike08</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Acompumike08" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=compumike08" title="Documentation">📖</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=compumike08" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/danielo515"><img src="https://avatars1.githubusercontent.com/u/2270425?v=3" width="100px;" alt=""/><br /><sub><b>Daniel Rodríguez Rivero</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Adanielo515" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=danielo515" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=danielo515" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/inyono"><img src="https://avatars2.githubusercontent.com/u/1508477?v=3" width="100px;" alt=""/><br /><sub><b>Jonas Keinholz</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Ainyono" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=inyono" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=inyono" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/hgwood"><img src="https://avatars3.githubusercontent.com/u/1656170?v=3" width="100px;" alt=""/><br /><sub><b>Hugo Wood</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Ahgwood" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=hgwood" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=hgwood" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/thomasthiebaud"><img src="https://avatars0.githubusercontent.com/u/3715715?v=3" width="100px;" alt=""/><br /><sub><b>Thiebaud Thomas</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Athomasthiebaud" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=thomasthiebaud" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=thomasthiebaud" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://daniel.blog"><img src="https://avatars1.githubusercontent.com/u/1715800?v=3" width="100px;" alt=""/><br /><sub><b>Daniel Rey López</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=DanReyLop" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=DanReyLop" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="http://amilajack.com"><img src="https://avatars2.githubusercontent.com/u/6374832?v=3" width="100px;" alt=""/><br /><sub><b>Amila Welihinda</b></sub></a><br /><a href="#infra-amilajack" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://twitter.com/paulcbetts"><img src="https://avatars1.githubusercontent.com/u/1396?v=3" width="100px;" alt=""/><br /><sub><b>Paul Betts</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Apaulcbetts" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=paulcbetts" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/turnerhayes"><img src="https://avatars1.githubusercontent.com/u/6371670?v=3" width="100px;" alt=""/><br /><sub><b>Turner Hayes</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Aturnerhayes" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=turnerhayes" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=turnerhayes" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/sudo-suhas"><img src="https://avatars2.githubusercontent.com/u/22251956?v=4" width="100px;" alt=""/><br /><sub><b>Suhas Karanth</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=sudo-suhas" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=sudo-suhas" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/sventschui"><img src="https://avatars3.githubusercontent.com/u/512692?v=4" width="100px;" alt=""/><br /><sub><b>Sven</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=sventschui" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=sventschui" title="Documentation">📖</a> <a href="#example-sventschui" title="Examples">💡</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=sventschui" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/NicoZelaya"><img src="https://avatars0.githubusercontent.com/u/5522668?v=4" width="100px;" alt=""/><br /><sub><b>D. Nicolás Lopez Zelaya</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=NicoZelaya" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://bithavoc.io"><img src="https://avatars3.githubusercontent.com/u/219289?v=4" width="100px;" alt=""/><br /><sub><b>Johan Hernandez</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=bithavoc" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jnielson94"><img src="https://avatars3.githubusercontent.com/u/13559161?v=4" width="100px;" alt=""/><br /><sub><b>Jordan Nielson</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/issues?q=author%3Ajnielson94" title="Bug reports">🐛</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=jnielson94" title="Code">💻</a> <a href="https://github.com/kentcdodds/cross-env/commits?author=jnielson94" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://nz.linkedin.com/in/jsonc11"><img src="https://avatars0.githubusercontent.com/u/5185660?v=4" width="100px;" alt=""/><br /><sub><b>Jason Cooke</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=Jason-Cooke" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/bibo5088"><img src="https://avatars0.githubusercontent.com/u/17709887?v=4" width="100px;" alt=""/><br /><sub><b>bibo5088</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=bibo5088" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://codefund.io"><img src="https://avatars2.githubusercontent.com/u/12481?v=4" width="100px;" alt=""/><br /><sub><b>Eric Berry</b></sub></a><br /><a href="#fundingFinding-coderberry" title="Funding Finding">🔍</a></td>
|
||||
<td align="center"><a href="https://michaeldeboey.be"><img src="https://avatars3.githubusercontent.com/u/6643991?v=4" width="100px;" alt=""/><br /><sub><b>Michaël De Boey</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=MichaelDeBoey" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/lauriii"><img src="https://avatars0.githubusercontent.com/u/1845495?v=4" width="100px;" alt=""/><br /><sub><b>Lauri Eskola</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=lauriii" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/devuxer"><img src="https://avatars0.githubusercontent.com/u/1298521?v=4" width="100px;" alt=""/><br /><sub><b>devuxer</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=devuxer" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/dsbert"><img src="https://avatars2.githubusercontent.com/u/1320090?v=4" width="100px;" alt=""/><br /><sub><b>Daniel</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=dsbert" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors][all-contributors] specification.
|
||||
Contributions of any kind welcome!
|
||||
|
||||
> Note: this was added late into the project. If you've contributed to this
|
||||
> project in any way, please make a pull request to add yourself to the list by
|
||||
> following the instructions in the `CONTRIBUTING.md`
|
||||
|
||||
## LICENSE
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[npm]: https://npmjs.com
|
||||
[node]: https://nodejs.org
|
||||
<!-- you caught me! The builds have been deleted, but I promise they were passing before now! -->
|
||||
[build-badge]: https://camo.githubusercontent.com/748e4a1386b345c47427adeb7025b74850b550e5b1deffecd6779c374b887184/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6b656e7463646f6464732f6d617463682d736f727465722f76616c69646174652e796d6c3f6c6f676f3d676974687562267374796c653d666c61742d737175617265266272616e63683d6d61696e
|
||||
[build]: https://github.com/kentcdodds/cross-env/actions?query=workflow%3Avalidate
|
||||
[coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/cross-env.svg?style=flat-square
|
||||
[coverage]: https://codecov.io/github/kentcdodds/cross-env
|
||||
[version-badge]: https://img.shields.io/npm/v/cross-env.svg?style=flat-square
|
||||
[package]: https://www.npmjs.com/package/cross-env
|
||||
[downloads-badge]: https://img.shields.io/npm/dm/cross-env.svg?style=flat-square
|
||||
[npmtrends]: http://www.npmtrends.com/cross-env
|
||||
[license-badge]: https://img.shields.io/npm/l/cross-env.svg?style=flat-square
|
||||
[license]: https://github.com/kentcdodds/cross-env/blob/master/LICENSE
|
||||
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
|
||||
[coc]: https://github.com/kentcdodds/cross-env/blob/master/other/CODE_OF_CONDUCT.md
|
||||
[emojis]: https://allcontributors.org/docs/en/emoji-key
|
||||
[all-contributors]: https://github.com/all-contributors/all-contributors
|
||||
[all-contributors-badge]: https://img.shields.io/github/all-contributors/kentcdodds/cross-env?color=orange&style=flat-square
|
||||
[bugs]: https://github.com/kentcdodds/cross-env/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3A%22%F0%9F%90%9B+Bug%22+sort%3Acreated-desc
|
||||
[good-first-issue]: https://github.com/kentcdodds/cross-env/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A%22good+first+issue%22
|
||||
|
||||
[angular-formly]: https://github.com/formly-js/angular-formly
|
||||
[cross-spawn]: https://www.npmjs.com/package/cross-spawn
|
||||
[malware]: http://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry
|
||||
[ts-loader]: https://www.npmjs.com/package/ts-loader
|
||||
[win-bash]: https://msdn.microsoft.com/en-us/commandline/wsl/about
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/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-shell binary
|
||||
const crossEnvShellPath = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'dist',
|
||||
'bin',
|
||||
'cross-env-shell.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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function runShellCommand(shellCommand, envVars = {}) {
|
||||
const envArgs = Object.entries(envVars).map(
|
||||
([key, value]) => `${key}=${value}`,
|
||||
)
|
||||
return runCommand(crossEnvShellPath, [...envArgs, shellCommand])
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('🧪 Running cross-env-shell e2e tests...')
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Basic shell environment variable',
|
||||
test: async () => {
|
||||
const result = await runShellCommand('echo $TEST_VAR', {
|
||||
TEST_VAR: 'hello world',
|
||||
})
|
||||
if (result.stdout.trim() !== 'hello world') {
|
||||
throw new Error(
|
||||
`Expected 'hello world', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Multiple shell environment variables',
|
||||
test: async () => {
|
||||
const result = await runShellCommand('echo "$VAR1 $VAR2"', {
|
||||
VAR1: 'hello',
|
||||
VAR2: 'world',
|
||||
})
|
||||
// The shell will handle this according to its own rules
|
||||
if (
|
||||
!result.stdout.trim().includes('hello') ||
|
||||
!result.stdout.trim().includes('world')
|
||||
) {
|
||||
throw new Error(
|
||||
`Expected output to contain 'hello' and 'world', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shell command with environment variable in command',
|
||||
test: async () => {
|
||||
const result = await runShellCommand('echo $MESSAGE', {
|
||||
MESSAGE: 'hello from shell',
|
||||
})
|
||||
if (!result.stdout.trim().includes('hello from shell')) {
|
||||
throw new Error(
|
||||
`Expected output to contain 'hello from shell', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shell command with environment variable substitution',
|
||||
test: async () => {
|
||||
const result = await runShellCommand('echo "Value is: $SUB_VAR"', {
|
||||
SUB_VAR: 'substituted',
|
||||
})
|
||||
if (!result.stdout.trim().includes('substituted')) {
|
||||
throw new Error(
|
||||
`Expected output to contain 'substituted', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shell command with special characters in env var',
|
||||
test: async () => {
|
||||
const result = await runShellCommand('echo "$SPECIAL_VAR"', {
|
||||
SPECIAL_VAR: 'test@#$%^&*()_+-=[]{}|;:,.<>?',
|
||||
})
|
||||
if (!result.stdout.trim().includes('test@#$%^&*()_+-=[]{}|;:,.<>?')) {
|
||||
throw new Error(
|
||||
`Expected output to contain special characters, 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 cross-env-shell e2e tests passed!')
|
||||
}
|
||||
|
||||
runTests().catch((error) => {
|
||||
console.error('❌ Test runner failed:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function runNodeScript(script, envVars = {}) {
|
||||
const envArgs = Object.entries(envVars).map(
|
||||
([key, value]) => `${key}=${value}`,
|
||||
)
|
||||
return runCommand(crossEnvPath, [...envArgs, 'node', '-e', script])
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log('🧪 Running cross-env e2e tests...')
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Basic environment variable setting',
|
||||
test: async () => {
|
||||
const result = await runNodeScript(
|
||||
'console.log(process.env.TEST_VAR)',
|
||||
{ TEST_VAR: 'hello world' },
|
||||
)
|
||||
if (result.stdout.trim() !== 'hello world') {
|
||||
throw new Error(
|
||||
`Expected 'hello world', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Multiple environment variables',
|
||||
test: async () => {
|
||||
const result = await runNodeScript(
|
||||
'console.log(process.env.VAR1 + " " + process.env.VAR2)',
|
||||
{ VAR1: 'hello', VAR2: 'world' },
|
||||
)
|
||||
if (result.stdout.trim() !== 'hello world') {
|
||||
throw new Error(
|
||||
`Expected 'hello world', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Environment variable with spaces',
|
||||
test: async () => {
|
||||
const result = await runNodeScript(
|
||||
'console.log(process.env.SPACE_VAR)',
|
||||
{ SPACE_VAR: 'hello world with spaces' },
|
||||
)
|
||||
if (result.stdout.trim() !== 'hello world with spaces') {
|
||||
throw new Error(
|
||||
`Expected 'hello world with spaces', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Environment variable with special characters',
|
||||
test: async () => {
|
||||
const result = await runNodeScript(
|
||||
'console.log(process.env.SPECIAL_VAR)',
|
||||
{ SPECIAL_VAR: 'test@#$%^&*()_+-=[]{}|;:,.<>?' },
|
||||
)
|
||||
if (result.stdout.trim() !== 'test@#$%^&*()_+-=[]{}|;:,.<>?') {
|
||||
throw new Error(
|
||||
`Expected 'test@#$%^&*()_+-=[]{}|;:,.<>?', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Empty environment variable',
|
||||
test: async () => {
|
||||
const result = await runNodeScript(
|
||||
'console.log("EMPTY:" + (process.env.EMPTY_VAR || "undefined"))',
|
||||
{ EMPTY_VAR: '' },
|
||||
)
|
||||
if (result.stdout.trim() !== 'EMPTY:undefined') {
|
||||
throw new Error(
|
||||
`Expected 'EMPTY:undefined', got '${result.stdout.trim()}'`,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Command with arguments',
|
||||
test: async () => {
|
||||
const result = await runCommand(crossEnvPath, [
|
||||
'TEST_ARG=value',
|
||||
'node',
|
||||
'-e',
|
||||
'console.log(process.argv[2] + " " + process.env.TEST_ARG)',
|
||||
'arg1',
|
||||
])
|
||||
// The actual behavior is that process.argv[2] is undefined when using -e
|
||||
if (result.stdout.trim() !== 'undefined value') {
|
||||
throw new Error(
|
||||
`Expected 'undefined value', 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 cross-env e2e tests passed!')
|
||||
}
|
||||
|
||||
runTests().catch((error) => {
|
||||
console.error('❌ Test runner failed:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import config from '@epic-web/config/eslint'
|
||||
|
||||
export default [
|
||||
...config,
|
||||
{
|
||||
ignores: ['dist/**', 'coverage/**'],
|
||||
},
|
||||
]
|
||||
@@ -1,6 +0,0 @@
|
||||
const jest = require('kcd-scripts/jest')
|
||||
|
||||
module.exports = {
|
||||
...jest,
|
||||
coveragePathIgnorePatterns: [...jest.coveragePathIgnorePatterns, '/bin/'],
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# Examples
|
||||
|
||||
There aren't any examples yet! Want to add one? See `CONTRIBUTING.md`
|
||||
@@ -1,11 +0,0 @@
|
||||
# Project Roadmap
|
||||
|
||||
This is where we'll define a few things about the library's goals.
|
||||
|
||||
We haven't filled this out yet though. Care to help? See `CONTRIBUTING.md`
|
||||
|
||||
## Want to do
|
||||
|
||||
## Might do
|
||||
|
||||
## Wont do
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Clean CJS files from the dist directory and update package.json types paths.
|
||||
*
|
||||
* This script removes .cjs and .d.cts files after zshy builds them,
|
||||
* and updates package.json to use .d.ts files for types instead of .d.cts.
|
||||
* This is a workaround until zshy supports ESM-only builds.
|
||||
*
|
||||
* See: https://github.com/colinhacks/zshy/issues/21
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
async function cleanCjsFiles(dir) {
|
||||
try {
|
||||
// Check if directory exists
|
||||
await fs.access(dir)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Directory does not exist or is not accessible: ${dir}`,
|
||||
error,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const items = await fs.readdir(dir, { withFileTypes: true })
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item.name)
|
||||
|
||||
if (item.isDirectory()) {
|
||||
await cleanCjsFiles(fullPath)
|
||||
} else if (item.name.endsWith('.cjs') || item.name.endsWith('.d.cts')) {
|
||||
try {
|
||||
console.log(`Deleting: ${fullPath}`)
|
||||
await fs.unlink(fullPath)
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete ${fullPath}:`, error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to read directory ${dir}:`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePackageJsonTypes() {
|
||||
const packageJsonPath = path.join(__dirname, '..', 'package.json')
|
||||
|
||||
try {
|
||||
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8')
|
||||
const packageJson = JSON.parse(packageJsonContent)
|
||||
|
||||
// Update the main field from .cjs to .js
|
||||
if (packageJson.main && packageJson.main.endsWith('.cjs')) {
|
||||
packageJson.main = packageJson.main.replace('.cjs', '.js')
|
||||
console.log(`Updated main: ${packageJson.main}`)
|
||||
}
|
||||
|
||||
// Update the main types field
|
||||
if (packageJson.types && packageJson.types.endsWith('.d.cts')) {
|
||||
packageJson.types = packageJson.types.replace('.d.cts', '.d.ts')
|
||||
console.log(`Updated main types: ${packageJson.types}`)
|
||||
}
|
||||
|
||||
// Update types in exports and remove require properties
|
||||
if (packageJson.exports) {
|
||||
for (const [key, value] of Object.entries(packageJson.exports)) {
|
||||
if (typeof value === 'object') {
|
||||
// Update types from .d.cts to .d.ts
|
||||
if (value.types && value.types.endsWith('.d.cts')) {
|
||||
value.types = value.types.replace('.d.cts', '.d.ts')
|
||||
console.log(`Updated types for ${key}: ${value.types}`)
|
||||
}
|
||||
|
||||
// Remove require properties since we're deleting CJS files
|
||||
if (value.require) {
|
||||
delete value.require
|
||||
console.log(`Removed require for ${key}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the updated package.json back
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2) + '\n',
|
||||
)
|
||||
console.log(
|
||||
'Updated package.json types paths and removed require properties',
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to update package.json:', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Clean the dist directory
|
||||
const distPath = path.join(__dirname, '..', 'dist')
|
||||
console.log('Cleaning CJS files from:', distPath)
|
||||
await cleanCjsFiles(distPath)
|
||||
console.log('Clean complete!')
|
||||
|
||||
// Update package.json types paths
|
||||
console.log('Updating package.json types paths...')
|
||||
await updatePackageJsonTypes()
|
||||
console.log('Update complete!')
|
||||
} catch (error) {
|
||||
console.error('Script failed:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
Generated
+7828
File diff suppressed because it is too large
Load Diff
+59
-24
@@ -2,47 +2,59 @@
|
||||
"name": "cross-env",
|
||||
"version": "0.0.0-semantically-released",
|
||||
"description": "Run scripts that set and use environment variables across platforms",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
"cross-env": "./dist/bin/cross-env.js",
|
||||
"cross-env-shell": "./dist/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "kcd-scripts lint",
|
||||
"setup": "npm install && npm run validate -s",
|
||||
"test": "kcd-scripts test",
|
||||
"test:update": "npm test -- --updateSnapshot --coverage",
|
||||
"validate": "kcd-scripts validate"
|
||||
"build": "zshy && npm run clean-cjs",
|
||||
"clean-cjs": "node other/clean-cjs.js",
|
||||
"dev": "zshy --watch",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"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",
|
||||
"validate": "npm run build && npm run typecheck && npm run lint && npm run format:check && npm run test:run",
|
||||
"prepublishOnly": "npm run validate"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"!__tests__"
|
||||
"dist"
|
||||
],
|
||||
"keywords": [
|
||||
"cross-environment",
|
||||
"environment variable",
|
||||
"windows"
|
||||
"windows",
|
||||
"cross-platform"
|
||||
],
|
||||
"author": "Kent C. Dodds <[email protected]> (https://kentcdodds.com)",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
"cross-spawn": "^7.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"kcd-scripts": "^5.5.0"
|
||||
"@epic-web/config": "^1.21.1",
|
||||
"eslint": "^9.32.0",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"zshy": "^0.2.5"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "./node_modules/kcd-scripts/eslint.js"
|
||||
},
|
||||
"// babel 1": "this disables all built-in plugins from kcd-scripts for tests",
|
||||
"// babel 2": "that way we ensure that the tests run without compilation",
|
||||
"// babel 3": "because this module is published as-is. It is not compiled.",
|
||||
"babel": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/kentcdodds/cross-env.git"
|
||||
@@ -50,5 +62,28 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/kentcdodds/cross-env/issues"
|
||||
},
|
||||
"homepage": "https://github.com/kentcdodds/cross-env#readme"
|
||||
"homepage": "https://github.com/kentcdodds/cross-env#readme",
|
||||
"zshy": {
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./bin/cross-env": "./src/bin/cross-env.ts",
|
||||
"./bin/cross-env-shell": "./src/bin/cross-env-shell.ts"
|
||||
}
|
||||
},
|
||||
"prettier": "@epic-web/config/prettier",
|
||||
"module": "./dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./bin/cross-env": {
|
||||
"types": "./dist/bin/cross-env.d.ts",
|
||||
"import": "./dist/bin/cross-env.js"
|
||||
},
|
||||
"./bin/cross-env-shell": {
|
||||
"types": "./dist/bin/cross-env-shell.d.ts",
|
||||
"import": "./dist/bin/cross-env-shell.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
const isWindowsMock = require('../is-windows')
|
||||
const commandConvert = require('../command')
|
||||
|
||||
jest.mock('../is-windows')
|
||||
|
||||
const env = {
|
||||
test: 'a',
|
||||
test1: 'b',
|
||||
test2: 'c',
|
||||
test3: 'd',
|
||||
// eslint-disable-next-line babel/camelcase
|
||||
empty_var: '',
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test(`converts unix-style env variable usage for windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(commandConvert('$test', env)).toBe('%test%')
|
||||
})
|
||||
|
||||
test(`leaves command unchanged when not a variable`, () => {
|
||||
expect(commandConvert('test', env)).toBe('test')
|
||||
})
|
||||
|
||||
test(`doesn't convert windows-style env variable`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(commandConvert('%test%', env)).toBe('%test%')
|
||||
})
|
||||
|
||||
test(`leaves variable unchanged when using correct operating system`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(commandConvert('$test', env)).toBe('$test')
|
||||
})
|
||||
|
||||
test(`is stateless`, () => {
|
||||
// this test prevents falling into regexp traps like this:
|
||||
// http://stackoverflow.com/a/1520853/971592
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(commandConvert('$test', env)).toBe(commandConvert('$test', env))
|
||||
})
|
||||
|
||||
test(`converts embedded unix-style env variables usage for windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(commandConvert('$test1/$test2/$test3', env)).toBe(
|
||||
'%test1%/%test2%/%test3%',
|
||||
)
|
||||
})
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
test(`leaves embedded variables unchanged when using correct operating system`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(commandConvert('$test1/$test2/$test3', env)).toBe(
|
||||
'$test1/$test2/$test3',
|
||||
)
|
||||
})
|
||||
|
||||
test(`converts braced unix-style env variable usage for windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
expect(commandConvert('${test}', env)).toBe('%test%')
|
||||
})
|
||||
|
||||
test(`removes non-existent variables from the converted command`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(commandConvert('$test1/$foo/$test2', env)).toBe('%test1%//%test2%')
|
||||
})
|
||||
|
||||
test(`removes empty variables from the converted command`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(commandConvert('$foo/$test/$empty_var', env)).toBe('/%test%/')
|
||||
})
|
||||
|
||||
test(`normalizes command on windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
// index.js calls `commandConvert` with `normalize` param
|
||||
// as `true` for command only
|
||||
expect(commandConvert('./cmd.bat', env, true)).toBe('cmd.bat')
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, test, expect, vi, afterEach } from 'vitest'
|
||||
import { commandConvert } from '../command.js'
|
||||
import * as isWindowsModule from '../is-windows.js'
|
||||
|
||||
vi.mock('../is-windows.js')
|
||||
|
||||
const env = {
|
||||
test: 'a',
|
||||
test1: 'b',
|
||||
test2: 'c',
|
||||
test3: 'd',
|
||||
empty_var: '',
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('commandConvert', () => {
|
||||
test('converts unix-style env variable usage for windows', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(commandConvert('$test', env)).toBe('%test%')
|
||||
})
|
||||
|
||||
test('leaves command unchanged when not a variable', () => {
|
||||
expect(commandConvert('test', env)).toBe('test')
|
||||
})
|
||||
|
||||
test("doesn't convert windows-style env variable", () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(commandConvert('%test%', env)).toBe('%test%')
|
||||
})
|
||||
|
||||
test('leaves variable unchanged when using correct operating system', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(commandConvert('$test', env)).toBe('$test')
|
||||
})
|
||||
|
||||
test('is stateless', () => {
|
||||
// this test prevents falling into regexp traps like this:
|
||||
// http://stackoverflow.com/a/1520853/971592
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(commandConvert('$test', env)).toBe(commandConvert('$test', env))
|
||||
})
|
||||
|
||||
test('converts embedded unix-style env variables usage for windows', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(commandConvert('$test1/$test2/$test3', env)).toBe(
|
||||
'%test1%/%test2%/%test3%',
|
||||
)
|
||||
})
|
||||
|
||||
test('leaves embedded variables unchanged when using correct operating system', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(commandConvert('$test1/$test2/$test3', env)).toBe(
|
||||
'$test1/$test2/$test3',
|
||||
)
|
||||
})
|
||||
|
||||
test('converts braced unix-style env variable usage for windows', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(commandConvert('${test}', env)).toBe('%test%')
|
||||
})
|
||||
|
||||
test('removes non-existent variables from the converted command', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(commandConvert('$test1/$foo/$test2', env)).toBe('%test1%//%test2%')
|
||||
})
|
||||
|
||||
test('removes empty variables from the converted command', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(commandConvert('$foo/$test/$empty_var', env)).toBe('/%test%/')
|
||||
})
|
||||
|
||||
test('normalizes command on windows', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
// index.js calls `commandConvert` with `normalize` param
|
||||
// as `true` for command only
|
||||
expect(commandConvert('./cmd.bat', env, true)).toBe('cmd.bat')
|
||||
})
|
||||
})
|
||||
@@ -1,213 +0,0 @@
|
||||
const crossSpawnMock = require('cross-spawn')
|
||||
const isWindowsMock = require('../is-windows')
|
||||
|
||||
jest.mock('../is-windows')
|
||||
jest.mock('cross-spawn')
|
||||
|
||||
const crossEnv = require('../')
|
||||
|
||||
const getSpawned = (call = 0) => crossSpawnMock.spawn.mock.results[call].value
|
||||
|
||||
process.setMaxListeners(20)
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(process, 'exit').mockImplementation(() => {})
|
||||
crossSpawnMock.spawn.mockReturnValue({on: jest.fn(), kill: jest.fn()})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
process.exit.mockRestore()
|
||||
})
|
||||
|
||||
test(`sets environment variables and run the remaining command`, () => {
|
||||
testEnvSetting({FOO_ENV: 'production'}, 'FOO_ENV=production')
|
||||
})
|
||||
|
||||
test(`APPDATA is undefined and not string`, () => {
|
||||
testEnvSetting({FOO_ENV: 'production', APPDATA: 2}, 'FOO_ENV=production')
|
||||
})
|
||||
|
||||
test(`handles multiple env variables`, () => {
|
||||
testEnvSetting(
|
||||
{
|
||||
FOO_ENV: 'production',
|
||||
BAR_ENV: 'dev',
|
||||
APPDATA: '0',
|
||||
},
|
||||
'FOO_ENV=production',
|
||||
'BAR_ENV=dev',
|
||||
'APPDATA=0',
|
||||
)
|
||||
})
|
||||
|
||||
test(`handles special characters`, () => {
|
||||
testEnvSetting({FOO_ENV: './!?'}, 'FOO_ENV=./!?')
|
||||
})
|
||||
|
||||
test(`handles single-quoted strings`, () => {
|
||||
testEnvSetting({FOO_ENV: 'bar env'}, "FOO_ENV='bar env'")
|
||||
})
|
||||
|
||||
test(`handles double-quoted strings`, () => {
|
||||
testEnvSetting({FOO_ENV: 'bar env'}, 'FOO_ENV="bar env"')
|
||||
})
|
||||
|
||||
test(`handles equality signs in quoted strings`, () => {
|
||||
testEnvSetting({FOO_ENV: 'foo=bar'}, 'FOO_ENV="foo=bar"')
|
||||
})
|
||||
|
||||
test(`handles empty single-quoted strings`, () => {
|
||||
testEnvSetting({FOO_ENV: ''}, "FOO_ENV=''")
|
||||
})
|
||||
|
||||
test(`handles empty double-quoted strings`, () => {
|
||||
testEnvSetting({FOO_ENV: ''}, 'FOO_ENV=""')
|
||||
})
|
||||
|
||||
test(`handles no value after the equals sign`, () => {
|
||||
testEnvSetting({FOO_ENV: ''}, 'FOO_ENV=')
|
||||
})
|
||||
|
||||
test(`handles quoted scripts`, () => {
|
||||
crossEnv(['GREETING=Hi', 'NAME=Joe', 'echo $GREETING && echo $NAME'], {
|
||||
shell: true,
|
||||
})
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith(
|
||||
'echo $GREETING && echo $NAME',
|
||||
[],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: {...process.env, GREETING: 'Hi', NAME: 'Joe'},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test(`handles escaped characters`, () => {
|
||||
// this escapes \,",' and $
|
||||
crossEnv(
|
||||
['GREETING=Hi', 'NAME=Joe', 'echo \\"\\\'\\$GREETING\\\'\\" && echo $NAME'],
|
||||
{
|
||||
shell: true,
|
||||
},
|
||||
)
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith(
|
||||
'echo "\'$GREETING\'" && echo $NAME',
|
||||
[],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: {...process.env, GREETING: 'Hi', NAME: 'Joe'},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test(`does nothing when given no command`, () => {
|
||||
crossEnv([])
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
test(`normalizes commands on windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
crossEnv(['./cmd.bat'])
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith('cmd.bat', [], {
|
||||
stdio: 'inherit',
|
||||
env: {...process.env},
|
||||
})
|
||||
})
|
||||
|
||||
test(`does not normalize command arguments on windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
crossEnv(['echo', 'http://example.com'])
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith(
|
||||
'echo',
|
||||
['http://example.com'],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: {...process.env},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test(`propagates kill signals`, () => {
|
||||
testEnvSetting({FOO_ENV: 'foo=bar'}, 'FOO_ENV="foo=bar"')
|
||||
|
||||
process.emit('SIGTERM')
|
||||
process.emit('SIGINT')
|
||||
process.emit('SIGHUP')
|
||||
process.emit('SIGBREAK')
|
||||
const spawned = getSpawned()
|
||||
expect(spawned.kill).toHaveBeenCalledWith('SIGTERM')
|
||||
expect(spawned.kill).toHaveBeenCalledWith('SIGINT')
|
||||
expect(spawned.kill).toHaveBeenCalledWith('SIGHUP')
|
||||
expect(spawned.kill).toHaveBeenCalledWith('SIGBREAK')
|
||||
})
|
||||
|
||||
test(`keeps backslashes`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
crossEnv(['echo', '\\\\\\\\someshare\\\\somefolder'])
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith(
|
||||
'echo',
|
||||
['\\\\someshare\\somefolder'],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: {...process.env},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test(`propagates unhandled exit signal`, () => {
|
||||
const {spawned} = testEnvSetting({FOO_ENV: 'foo=bar'}, 'FOO_ENV="foo=bar"')
|
||||
const spawnExitCallback = spawned.on.mock.calls[0][1]
|
||||
const spawnExitCode = null
|
||||
spawnExitCallback(spawnExitCode)
|
||||
expect(process.exit).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
test(`exits cleanly with SIGINT with a null exit code`, () => {
|
||||
const {spawned} = testEnvSetting({FOO_ENV: 'foo=bar'}, 'FOO_ENV="foo=bar"')
|
||||
const spawnExitCallback = spawned.on.mock.calls[0][1]
|
||||
const spawnExitCode = null
|
||||
const spawnExitSignal = 'SIGINT'
|
||||
spawnExitCallback(spawnExitCode, spawnExitSignal)
|
||||
expect(process.exit).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
test(`propagates regular exit code`, () => {
|
||||
const {spawned} = testEnvSetting({FOO_ENV: 'foo=bar'}, 'FOO_ENV="foo=bar"')
|
||||
const spawnExitCallback = spawned.on.mock.calls[0][1]
|
||||
const spawnExitCode = 0
|
||||
spawnExitCallback(spawnExitCode)
|
||||
expect(process.exit).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
function testEnvSetting(expected, ...envSettings) {
|
||||
if (expected.APPDATA === 2) {
|
||||
// kill the APPDATA to test both is undefined
|
||||
const {env} = process
|
||||
delete env.APPDATA
|
||||
delete expected.APPDATA
|
||||
} else if (!process.env.APPDATA && expected.APPDATA === '0') {
|
||||
// set APPDATA and test it
|
||||
process.env.APPDATA = '0'
|
||||
}
|
||||
const ret = crossEnv([...envSettings, 'echo', 'hello world'])
|
||||
const env = {}
|
||||
if (process.env.APPDATA) {
|
||||
env.APPDATA = process.env.APPDATA
|
||||
}
|
||||
Object.assign(env, expected)
|
||||
const spawned = getSpawned()
|
||||
expect(ret).toBe(spawned)
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledTimes(1)
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith('echo', ['hello world'], {
|
||||
stdio: 'inherit',
|
||||
shell: undefined,
|
||||
env: {...process.env, ...env},
|
||||
})
|
||||
|
||||
expect(spawned.on).toHaveBeenCalledTimes(1)
|
||||
expect(spawned.on).toHaveBeenCalledWith('exit', expect.any(Function))
|
||||
return {spawned}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import * as crossSpawnModule from 'cross-spawn'
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { crossEnv } from '../index.js'
|
||||
import * as isWindowsModule from '../is-windows.js'
|
||||
|
||||
vi.mock('../is-windows.js')
|
||||
vi.mock('cross-spawn')
|
||||
|
||||
const crossSpawnMock = vi.mocked(crossSpawnModule)
|
||||
const isWindowsMock = vi.mocked(isWindowsModule.isWindows)
|
||||
|
||||
const getSpawned = (call = 0) =>
|
||||
vi.mocked(crossSpawnMock.spawn).mock.results[call]?.value
|
||||
|
||||
process.setMaxListeners(20)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
|
||||
vi.mocked(crossSpawnMock.spawn).mockReturnValue({
|
||||
on: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
} as any)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(process.exit).mockRestore()
|
||||
})
|
||||
|
||||
describe('crossEnv', () => {
|
||||
test('sets environment variables and run the remaining command', () => {
|
||||
testEnvSetting({ FOO_ENV: 'production' }, 'FOO_ENV=production')
|
||||
})
|
||||
|
||||
test('APPDATA is undefined and not string', () => {
|
||||
testEnvSetting({ FOO_ENV: 'production', APPDATA: 2 }, 'FOO_ENV=production')
|
||||
})
|
||||
|
||||
test('handles multiple env variables', () => {
|
||||
testEnvSetting(
|
||||
{
|
||||
FOO_ENV: 'production',
|
||||
BAR_ENV: 'dev',
|
||||
APPDATA: '0',
|
||||
},
|
||||
'FOO_ENV=production',
|
||||
'BAR_ENV=dev',
|
||||
'APPDATA=0',
|
||||
)
|
||||
})
|
||||
|
||||
test('handles special characters', () => {
|
||||
testEnvSetting({ FOO_ENV: './!?' }, 'FOO_ENV=./!?')
|
||||
})
|
||||
|
||||
test('handles single-quoted strings', () => {
|
||||
testEnvSetting({ FOO_ENV: 'bar env' }, "FOO_ENV='bar env'")
|
||||
})
|
||||
|
||||
test('handles double-quoted strings', () => {
|
||||
testEnvSetting({ FOO_ENV: 'bar env' }, 'FOO_ENV="bar env"')
|
||||
})
|
||||
|
||||
test('handles equality signs in quoted strings', () => {
|
||||
testEnvSetting({ FOO_ENV: 'foo=bar' }, 'FOO_ENV="foo=bar"')
|
||||
})
|
||||
|
||||
test('handles empty single-quoted strings', () => {
|
||||
testEnvSetting({ FOO_ENV: '' }, "FOO_ENV=''")
|
||||
})
|
||||
|
||||
test('handles empty double-quoted strings', () => {
|
||||
testEnvSetting({ FOO_ENV: '' }, 'FOO_ENV=""')
|
||||
})
|
||||
|
||||
test('handles no value after the equals sign', () => {
|
||||
testEnvSetting({ FOO_ENV: '' }, 'FOO_ENV=')
|
||||
})
|
||||
|
||||
test('handles quoted scripts', () => {
|
||||
crossEnv(['GREETING=Hi', 'NAME=Joe', 'echo $GREETING && echo $NAME'], {
|
||||
shell: true,
|
||||
})
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith(
|
||||
'echo $GREETING && echo $NAME',
|
||||
[],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: { ...process.env, GREETING: 'Hi', NAME: 'Joe' },
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('handles escaped characters', () => {
|
||||
// this escapes \,",' and $
|
||||
crossEnv(
|
||||
[
|
||||
'GREETING=Hi',
|
||||
'NAME=Joe',
|
||||
'echo \\"\\\'\\$GREETING\\\'\\" && echo $NAME',
|
||||
],
|
||||
{
|
||||
shell: true,
|
||||
},
|
||||
)
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith(
|
||||
'echo "\'$GREETING\'" && echo $NAME',
|
||||
[],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: { ...process.env, GREETING: 'Hi', NAME: 'Joe' },
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('does nothing when given no command', () => {
|
||||
crossEnv([])
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
test('normalizes commands on windows', () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
crossEnv(['./cmd.bat'])
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith('cmd.bat', [], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env },
|
||||
})
|
||||
})
|
||||
|
||||
test('does not normalize command arguments on windows', () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
crossEnv(['echo', 'http://example.com'])
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith(
|
||||
'echo',
|
||||
['http://example.com'],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env },
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('propagates kill signals', () => {
|
||||
testEnvSetting({ FOO_ENV: 'foo=bar' }, 'FOO_ENV="foo=bar"')
|
||||
|
||||
process.emit('SIGTERM')
|
||||
process.emit('SIGINT')
|
||||
process.emit('SIGHUP')
|
||||
process.emit('SIGBREAK')
|
||||
const spawned = getSpawned()
|
||||
expect(spawned?.kill).toHaveBeenCalledWith('SIGTERM')
|
||||
expect(spawned?.kill).toHaveBeenCalledWith('SIGINT')
|
||||
expect(spawned?.kill).toHaveBeenCalledWith('SIGHUP')
|
||||
expect(spawned?.kill).toHaveBeenCalledWith('SIGBREAK')
|
||||
})
|
||||
|
||||
test('keeps backslashes', () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
crossEnv(['echo', '\\\\\\\\someshare\\\\somefolder'])
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith(
|
||||
'echo',
|
||||
['\\\\someshare\\somefolder'],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env },
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('propagates unhandled exit signal', () => {
|
||||
const { spawned } = testEnvSetting(
|
||||
{ FOO_ENV: 'foo=bar' },
|
||||
'FOO_ENV="foo=bar"',
|
||||
)
|
||||
const spawnExitCallback = spawned?.on.mock.calls[0]?.[1] as (
|
||||
code: number | null,
|
||||
signal?: string,
|
||||
) => void
|
||||
const spawnExitCode = null
|
||||
spawnExitCallback(spawnExitCode)
|
||||
expect(process.exit).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
test('exits cleanly with SIGINT with a null exit code', () => {
|
||||
const { spawned } = testEnvSetting(
|
||||
{ FOO_ENV: 'foo=bar' },
|
||||
'FOO_ENV="foo=bar"',
|
||||
)
|
||||
const spawnExitCallback = spawned?.on.mock.calls[0]?.[1] as (
|
||||
code: number | null,
|
||||
signal?: string,
|
||||
) => void
|
||||
const spawnExitCode = null
|
||||
const spawnExitSignal = 'SIGINT'
|
||||
spawnExitCallback(spawnExitCode, spawnExitSignal)
|
||||
expect(process.exit).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
test('propagates regular exit code', () => {
|
||||
const { spawned } = testEnvSetting(
|
||||
{ FOO_ENV: 'foo=bar' },
|
||||
'FOO_ENV="foo=bar"',
|
||||
)
|
||||
const spawnExitCallback = spawned?.on.mock.calls[0]?.[1] as (
|
||||
code: number | null,
|
||||
signal?: string,
|
||||
) => void
|
||||
const spawnExitCode = 0
|
||||
spawnExitCallback(spawnExitCode)
|
||||
expect(process.exit).toHaveBeenCalledWith(0)
|
||||
})
|
||||
})
|
||||
|
||||
function testEnvSetting(
|
||||
expected: Record<string, any>,
|
||||
...envSettings: string[]
|
||||
) {
|
||||
if (expected.APPDATA === 2) {
|
||||
// kill the APPDATA to test both is undefined
|
||||
const { env } = process
|
||||
delete env.APPDATA
|
||||
delete expected.APPDATA
|
||||
} else if (!process.env.APPDATA && expected.APPDATA === '0') {
|
||||
// set APPDATA and test it
|
||||
process.env.APPDATA = '0'
|
||||
}
|
||||
const ret = crossEnv([...envSettings, 'echo', 'hello world'])
|
||||
const env: Record<string, string> = {}
|
||||
if (process.env.APPDATA) {
|
||||
env.APPDATA = process.env.APPDATA
|
||||
}
|
||||
Object.assign(env, expected)
|
||||
const spawned = getSpawned()
|
||||
expect(ret).toBe(spawned)
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledTimes(1)
|
||||
expect(crossSpawnMock.spawn).toHaveBeenCalledWith('echo', ['hello world'], {
|
||||
stdio: 'inherit',
|
||||
shell: undefined,
|
||||
env: { ...process.env, ...env },
|
||||
})
|
||||
|
||||
expect(spawned?.on).toHaveBeenCalledTimes(1)
|
||||
expect(spawned?.on).toHaveBeenCalledWith('exit', expect.any(Function))
|
||||
return { spawned }
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
const isWindows = require('../is-windows')
|
||||
|
||||
const {
|
||||
platform,
|
||||
env: {OSTYPE},
|
||||
} = process
|
||||
|
||||
// make the platform property writeable
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: platform,
|
||||
writable: true,
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.platform = platform
|
||||
process.env.OSTYPE = OSTYPE
|
||||
})
|
||||
|
||||
test(`returns true if the current OS is Windows`, () => {
|
||||
process.platform = 'win32'
|
||||
expect(isWindows()).toBe(true)
|
||||
})
|
||||
|
||||
test(`returns false if the current OS is not Windows`, () => {
|
||||
process.platform = 'linux'
|
||||
expect(isWindows()).toBe(false)
|
||||
})
|
||||
|
||||
test(`returns true if the OSTYPE is cygwin or msys`, () => {
|
||||
process.platform = 'linux'
|
||||
|
||||
process.env.OSTYPE = 'cygwin'
|
||||
expect(isWindows()).toBe(true)
|
||||
|
||||
process.env.OSTYPE = 'msys'
|
||||
expect(isWindows()).toBe(true)
|
||||
|
||||
process.env.OSTYPE = ''
|
||||
expect(isWindows()).toBe(false)
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, test, expect, afterEach, vi } from 'vitest'
|
||||
import { isWindows } from '../is-windows.js'
|
||||
|
||||
const {
|
||||
env: { OSTYPE },
|
||||
} = process
|
||||
|
||||
afterEach(() => {
|
||||
process.env.OSTYPE = OSTYPE
|
||||
})
|
||||
|
||||
describe('isWindows', () => {
|
||||
test('returns true if the current OS is Windows', () => {
|
||||
vi.stubGlobal('process', { ...process, platform: 'win32' })
|
||||
expect(isWindows()).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false if the current OS is not Windows', () => {
|
||||
vi.stubGlobal('process', { ...process, platform: 'linux' })
|
||||
expect(isWindows()).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true if the OSTYPE is cygwin or msys', () => {
|
||||
vi.stubGlobal('process', { ...process, platform: 'linux' })
|
||||
|
||||
process.env.OSTYPE = 'cygwin'
|
||||
expect(isWindows()).toBe(true)
|
||||
|
||||
process.env.OSTYPE = 'msys'
|
||||
expect(isWindows()).toBe(true)
|
||||
|
||||
process.env.OSTYPE = ''
|
||||
expect(isWindows()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,121 +0,0 @@
|
||||
const isWindowsMock = require('../is-windows')
|
||||
const varValueConvert = require('../variable')
|
||||
|
||||
jest.mock('../is-windows')
|
||||
|
||||
const JSON_VALUE = '{\\"foo\\":\\"bar\\"}'
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.VAR1 = 'value1'
|
||||
process.env.VAR2 = 'value2'
|
||||
process.env.JSON_VAR = JSON_VALUE
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
const {env} = process
|
||||
delete env.VAR1
|
||||
delete env.VAR2
|
||||
delete env.JSON_VAR
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test(`doesn't affect simple variable values`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo')).toBe('foo')
|
||||
})
|
||||
|
||||
test(`doesn't convert a ; into a : on UNIX`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(varValueConvert('foo;bar', 'PATH')).toBe('foo;bar')
|
||||
})
|
||||
|
||||
test(`doesn't convert a ; into a : for non-PATH on UNIX`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(varValueConvert('foo;bar', 'FOO')).toBe('foo;bar')
|
||||
})
|
||||
|
||||
test(`doesn't convert a ; into a : for non-PATH on Windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo;bar', 'FOO')).toBe('foo;bar')
|
||||
})
|
||||
|
||||
test(`converts a : into a ; on Windows if PATH`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo:bar', 'PATH')).toBe('foo;bar')
|
||||
})
|
||||
|
||||
test(`doesn't convert already valid separators`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(varValueConvert('foo:bar')).toBe('foo:bar')
|
||||
})
|
||||
|
||||
test(`doesn't convert escaped separators on Windows if PATH`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo\\:bar', 'PATH')).toBe('foo:bar')
|
||||
})
|
||||
|
||||
test(`doesn't convert escaped separators on UNIX`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(varValueConvert('foo\\:bar', 'PATH')).toBe('foo:bar')
|
||||
})
|
||||
|
||||
test(`converts a separator even if preceded by an escaped backslash`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo\\\\:bar', 'PATH')).toBe('foo\\\\;bar')
|
||||
})
|
||||
|
||||
test(`converts multiple separators if PATH`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo:bar:baz', 'PATH')).toBe('foo;bar;baz')
|
||||
})
|
||||
|
||||
test(`resolves an env variable value`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo-$VAR1')).toBe('foo-value1')
|
||||
})
|
||||
|
||||
test(`resolves an env variable value with curly syntax`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
expect(varValueConvert('foo-${VAR1}')).toBe('foo-value1')
|
||||
})
|
||||
|
||||
test(`resolves multiple env variable values`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo-$VAR1-$VAR2')).toBe('foo-value1-value2')
|
||||
})
|
||||
|
||||
test(`resolves an env variable value for non-existant variable`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('foo-$VAR_POTATO')).toBe('foo-')
|
||||
})
|
||||
|
||||
test(`resolves an env variable with a JSON string value on Windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('$JSON_VAR')).toBe(JSON_VALUE)
|
||||
})
|
||||
|
||||
test(`resolves an env variable with a JSON string value on UNIX`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(varValueConvert('$JSON_VAR')).toBe(JSON_VALUE)
|
||||
})
|
||||
|
||||
test(`does not resolve an env variable prefixed with \\ on Windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('\\$VAR1')).toBe('$VAR1')
|
||||
})
|
||||
|
||||
test(`does not resolve an env variable prefixed with \\ on UNIX`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(varValueConvert('\\$VAR1')).toBe('$VAR1')
|
||||
})
|
||||
|
||||
test(`resolves an env variable prefixed with \\\\ on Windows`, () => {
|
||||
isWindowsMock.mockReturnValue(true)
|
||||
expect(varValueConvert('\\\\$VAR1')).toBe('\\value1')
|
||||
})
|
||||
|
||||
test(`resolves an env variable prefixed with \\\\ on UNIX`, () => {
|
||||
isWindowsMock.mockReturnValue(false)
|
||||
expect(varValueConvert('\\\\$VAR1')).toBe('\\value1')
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import * as isWindowsModule from '../is-windows.js'
|
||||
import { varValueConvert } from '../variable.js'
|
||||
|
||||
vi.mock('../is-windows.js')
|
||||
|
||||
const JSON_VALUE = '{\\"foo\\":\\"bar\\"}'
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.VAR1 = 'value1'
|
||||
process.env.VAR2 = 'value2'
|
||||
process.env.JSON_VAR = JSON_VALUE
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
const { env } = process
|
||||
delete env.VAR1
|
||||
delete env.VAR2
|
||||
delete env.JSON_VAR
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('varValueConvert', () => {
|
||||
test("doesn't affect simple variable values", () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo', 'TEST')).toBe('foo')
|
||||
})
|
||||
|
||||
test("doesn't convert a ; into a : on UNIX", () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(varValueConvert('foo;bar', 'PATH')).toBe('foo;bar')
|
||||
})
|
||||
|
||||
test("doesn't convert a ; into a : for non-PATH on UNIX", () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(varValueConvert('foo;bar', 'FOO')).toBe('foo;bar')
|
||||
})
|
||||
|
||||
test("doesn't convert a ; into a : for non-PATH on Windows", () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo;bar', 'FOO')).toBe('foo;bar')
|
||||
})
|
||||
|
||||
test('converts a : into a ; on Windows if PATH', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo:bar', 'PATH')).toBe('foo;bar')
|
||||
})
|
||||
|
||||
test("doesn't convert already valid separators", () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(varValueConvert('foo:bar', 'TEST')).toBe('foo:bar')
|
||||
})
|
||||
|
||||
test("doesn't convert escaped separators on Windows if PATH", () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo\\:bar', 'PATH')).toBe('foo:bar')
|
||||
})
|
||||
|
||||
test("doesn't convert escaped separators on UNIX", () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(varValueConvert('foo\\:bar', 'PATH')).toBe('foo:bar')
|
||||
})
|
||||
|
||||
test('converts a separator even if preceded by an escaped backslash', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo\\\\:bar', 'PATH')).toBe('foo\\\\;bar')
|
||||
})
|
||||
|
||||
test('converts multiple separators if PATH', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo:bar:baz', 'PATH')).toBe('foo;bar;baz')
|
||||
})
|
||||
|
||||
test('resolves an env variable value', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo-$VAR1', 'TEST')).toBe('foo-value1')
|
||||
})
|
||||
|
||||
test('resolves an env variable value with curly syntax', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo-${VAR1}', 'TEST')).toBe('foo-value1')
|
||||
})
|
||||
|
||||
test('resolves multiple env variable values', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo-$VAR1-$VAR2', 'TEST')).toBe('foo-value1-value2')
|
||||
})
|
||||
|
||||
test('resolves an env variable value for non-existant variable', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('foo-$VAR_POTATO', 'TEST')).toBe('foo-')
|
||||
})
|
||||
|
||||
test('resolves an env variable with a JSON string value on Windows', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('$JSON_VAR', 'TEST')).toBe(JSON_VALUE)
|
||||
})
|
||||
|
||||
test('resolves an env variable with a JSON string value on UNIX', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(varValueConvert('$JSON_VAR', 'TEST')).toBe(JSON_VALUE)
|
||||
})
|
||||
|
||||
test('does not resolve an env variable prefixed with \\ on Windows', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('\\$VAR1', 'TEST')).toBe('$VAR1')
|
||||
})
|
||||
|
||||
test('does not resolve an env variable prefixed with \\ on UNIX', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(varValueConvert('\\$VAR1', 'TEST')).toBe('$VAR1')
|
||||
})
|
||||
|
||||
test('resolves an env variable prefixed with \\\\ on Windows', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(true)
|
||||
expect(varValueConvert('\\\\$VAR1', 'TEST')).toBe('\\value1')
|
||||
})
|
||||
|
||||
test('resolves an env variable prefixed with \\\\ on UNIX', () => {
|
||||
vi.mocked(isWindowsModule.isWindows).mockReturnValue(false)
|
||||
expect(varValueConvert('\\\\$VAR1', 'TEST')).toBe('\\value1')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const crossEnv = require('..')
|
||||
|
||||
crossEnv(process.argv.slice(2), {shell: true})
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { crossEnv } from '../index.js'
|
||||
|
||||
crossEnv(process.argv.slice(2), { shell: true })
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const crossEnv = require('..')
|
||||
import { crossEnv } from '../index.js'
|
||||
|
||||
crossEnv(process.argv.slice(2))
|
||||
@@ -1,32 +0,0 @@
|
||||
const path = require('path')
|
||||
const isWindows = require('./is-windows')
|
||||
|
||||
module.exports = commandConvert
|
||||
|
||||
/**
|
||||
* Converts an environment variable usage to be appropriate for the current OS
|
||||
* @param {String} command Command to convert
|
||||
* @param {Object} env Map of the current environment variable names and their values
|
||||
* @param {boolean} normalize If the command should be normalized using `path`
|
||||
* after converting
|
||||
* @returns {String} Converted command
|
||||
*/
|
||||
function commandConvert(command, env, normalize = false) {
|
||||
if (!isWindows()) {
|
||||
return command
|
||||
}
|
||||
const envUnixRegex = /\$(\w+)|\${(\w+)}/g // $my_var or ${my_var}
|
||||
const convertedCmd = command.replace(envUnixRegex, (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
|
||||
// opposed to printing an empty string in UNIX. See kentcdodds/cross-env#145
|
||||
// If the env variable isn't defined at runtime, just strip it from the command entirely
|
||||
return env[varName] ? `%${varName}%` : ''
|
||||
})
|
||||
// Normalization is required for commands with relative paths
|
||||
// For example, `./cmd.bat`. See kentcdodds/cross-env#127
|
||||
// However, it should not be done for command arguments.
|
||||
// See https://github.com/kentcdodds/cross-env/pull/130#issuecomment-319887970
|
||||
return normalize === true ? path.normalize(convertedCmd) : convertedCmd
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import path from 'path'
|
||||
import { isWindows } from './is-windows.js'
|
||||
|
||||
/**
|
||||
* Converts an environment variable usage to be appropriate for the current OS
|
||||
* @param command Command to convert
|
||||
* @param env Map of the current environment variable names and their values
|
||||
* @param normalize If the command should be normalized using `path` after converting
|
||||
* @returns Converted command
|
||||
*/
|
||||
export function commandConvert(
|
||||
command: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
normalize = false,
|
||||
): string {
|
||||
if (!isWindows()) {
|
||||
return command
|
||||
}
|
||||
|
||||
const envUnixRegex = /\$(\w+)|\${(\w+)}/g // $my_var or ${my_var}
|
||||
const convertedCmd = command.replace(envUnixRegex, (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
|
||||
// opposed to printing an empty string in UNIX. See kentcdodds/cross-env#145
|
||||
// If the env variable isn't defined at runtime, just strip it from the command entirely
|
||||
return env[varName] ? `%${varName}%` : ''
|
||||
})
|
||||
|
||||
// Normalization is required for commands with relative paths
|
||||
// For example, `./cmd.bat`. See kentcdodds/cross-env#127
|
||||
// However, it should not be done for command arguments.
|
||||
// See https://github.com/kentcdodds/cross-env/pull/130#issuecomment-319887970
|
||||
return normalize === true ? path.normalize(convertedCmd) : convertedCmd
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
const {spawn} = require('cross-spawn')
|
||||
const commandConvert = require('./command')
|
||||
const varValueConvert = require('./variable')
|
||||
|
||||
module.exports = crossEnv
|
||||
|
||||
const envSetterRegex = /(\w+)=('(.*)'|"(.*)"|(.*))/
|
||||
|
||||
function crossEnv(args, options = {}) {
|
||||
const [envSetters, command, commandArgs] = parseCommand(args)
|
||||
const env = getEnvVars(envSetters)
|
||||
if (command) {
|
||||
const proc = spawn(
|
||||
// run `path.normalize` for command(on windows)
|
||||
commandConvert(command, env, true),
|
||||
// by default normalize is `false`, so not run for cmd args
|
||||
commandArgs.map(arg => commandConvert(arg, env)),
|
||||
{
|
||||
stdio: 'inherit',
|
||||
shell: options.shell,
|
||||
env,
|
||||
},
|
||||
)
|
||||
process.on('SIGTERM', () => proc.kill('SIGTERM'))
|
||||
process.on('SIGINT', () => proc.kill('SIGINT'))
|
||||
process.on('SIGBREAK', () => proc.kill('SIGBREAK'))
|
||||
process.on('SIGHUP', () => proc.kill('SIGHUP'))
|
||||
proc.on('exit', (code, signal) => {
|
||||
let crossEnvExitCode = code
|
||||
// exit code could be null when OS kills the process(out of memory, etc) or due to node handling it
|
||||
// but if the signal is SIGINT the user exited the process so we want exit code 0
|
||||
if (crossEnvExitCode === null) {
|
||||
crossEnvExitCode = signal === 'SIGINT' ? 0 : 1
|
||||
}
|
||||
process.exit(crossEnvExitCode) //eslint-disable-line no-process-exit
|
||||
})
|
||||
return proc
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function parseCommand(args) {
|
||||
const envSetters = {}
|
||||
let command = null
|
||||
let commandArgs = []
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const match = envSetterRegex.exec(args[i])
|
||||
if (match) {
|
||||
let value
|
||||
|
||||
if (typeof match[3] !== 'undefined') {
|
||||
value = match[3]
|
||||
} else if (typeof match[4] === 'undefined') {
|
||||
value = match[5]
|
||||
} else {
|
||||
value = match[4]
|
||||
}
|
||||
|
||||
envSetters[match[1]] = value
|
||||
} else {
|
||||
// No more env setters, the rest of the line must be the command and args
|
||||
let cStart = []
|
||||
cStart = args
|
||||
.slice(i)
|
||||
// Regex:
|
||||
// match "\'" or "'"
|
||||
// or match "\" if followed by [$"\] (lookahead)
|
||||
.map(a => {
|
||||
const re = /\\\\|(\\)?'|([\\])(?=[$"\\])/g
|
||||
// Eliminate all matches except for "\'" => "'"
|
||||
return a.replace(re, m => {
|
||||
if (m === '\\\\') return '\\'
|
||||
if (m === "\\'") return "'"
|
||||
return ''
|
||||
})
|
||||
})
|
||||
command = cStart[0]
|
||||
commandArgs = cStart.slice(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [envSetters, command, commandArgs]
|
||||
}
|
||||
|
||||
function getEnvVars(envSetters) {
|
||||
const envVars = {...process.env}
|
||||
if (process.env.APPDATA) {
|
||||
envVars.APPDATA = process.env.APPDATA
|
||||
}
|
||||
Object.keys(envSetters).forEach(varName => {
|
||||
envVars[varName] = varValueConvert(envSetters[varName], varName)
|
||||
})
|
||||
return envVars
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { type SpawnOptions } from 'child_process'
|
||||
import { spawn } from 'cross-spawn'
|
||||
import { commandConvert } from './command.js'
|
||||
import { varValueConvert } from './variable.js'
|
||||
|
||||
export type CrossEnvOptions = {
|
||||
shell?: boolean
|
||||
}
|
||||
|
||||
export type ProcessResult = {
|
||||
exitCode: number | null
|
||||
signal?: string | null
|
||||
}
|
||||
|
||||
const envSetterRegex = /(\w+)=('(.*)'|"(.*)"|(.*))/
|
||||
|
||||
export function crossEnv(
|
||||
args: string[],
|
||||
options: CrossEnvOptions = {},
|
||||
): ProcessResult | null {
|
||||
const [envSetters, command, commandArgs] = parseCommand(args)
|
||||
const env = getEnvVars(envSetters)
|
||||
|
||||
if (command) {
|
||||
const spawnOptions: SpawnOptions = {
|
||||
stdio: 'inherit',
|
||||
shell: options.shell,
|
||||
env,
|
||||
}
|
||||
|
||||
const proc = spawn(
|
||||
// run `path.normalize` for command(on windows)
|
||||
commandConvert(command, env, true),
|
||||
// by default normalize is `false`, so not run for cmd args
|
||||
commandArgs.map((arg) => commandConvert(arg, env)),
|
||||
spawnOptions,
|
||||
)
|
||||
|
||||
process.on('SIGTERM', () => proc.kill('SIGTERM'))
|
||||
process.on('SIGINT', () => proc.kill('SIGINT'))
|
||||
process.on('SIGBREAK', () => proc.kill('SIGBREAK'))
|
||||
process.on('SIGHUP', () => proc.kill('SIGHUP'))
|
||||
|
||||
proc.on('exit', (code: number | null, signal?: string) => {
|
||||
let crossEnvExitCode = code
|
||||
// exit code could be null when OS kills the process(out of memory, etc) or due to node handling it
|
||||
// but if the signal is SIGINT the user exited the process so we want exit code 0
|
||||
if (crossEnvExitCode === null) {
|
||||
crossEnvExitCode = signal === 'SIGINT' ? 0 : 1
|
||||
}
|
||||
process.exit(crossEnvExitCode)
|
||||
})
|
||||
|
||||
return proc
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseCommand(
|
||||
args: string[],
|
||||
): [Record<string, string>, string | null, string[]] {
|
||||
const envSetters: Record<string, string> = {}
|
||||
let command: string | null = null
|
||||
let commandArgs: string[] = []
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i]
|
||||
if (!arg) continue
|
||||
const match = envSetterRegex.exec(arg)
|
||||
if (match && match[1]) {
|
||||
let value: string
|
||||
|
||||
if (typeof match[3] !== 'undefined') {
|
||||
value = match[3]
|
||||
} else if (typeof match[4] === 'undefined') {
|
||||
value = match[5] || ''
|
||||
} else {
|
||||
value = match[4]
|
||||
}
|
||||
|
||||
envSetters[match[1]] = value
|
||||
} else {
|
||||
// No more env setters, the rest of the line must be the command and args
|
||||
const cStart = args
|
||||
.slice(i)
|
||||
// Regex:
|
||||
// match "\'" or "'"
|
||||
// or match "\" if followed by [$"\] (lookahead)
|
||||
.map((a) => {
|
||||
const re = /\\\\|(\\)?'|([\\])(?=[$"\\])/g
|
||||
// Eliminate all matches except for "\'" => "'"
|
||||
return a.replace(re, (m) => {
|
||||
if (m === '\\\\') return '\\'
|
||||
if (m === "\\'") return "'"
|
||||
return ''
|
||||
})
|
||||
})
|
||||
command = cStart[0] || null
|
||||
commandArgs = cStart.slice(1).filter(Boolean)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [envSetters, command, commandArgs]
|
||||
}
|
||||
|
||||
function getEnvVars(envSetters: Record<string, string>): NodeJS.ProcessEnv {
|
||||
const envVars = { ...process.env }
|
||||
if (process.env.APPDATA) {
|
||||
envVars.APPDATA = process.env.APPDATA
|
||||
}
|
||||
Object.keys(envSetters).forEach((varName) => {
|
||||
const value = envSetters[varName]
|
||||
if (value !== undefined) {
|
||||
envVars[varName] = varValueConvert(value, varName)
|
||||
}
|
||||
})
|
||||
return envVars
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
module.exports = () =>
|
||||
process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Determines if the current platform is Windows
|
||||
* @returns true if running on Windows, false otherwise
|
||||
*/
|
||||
export function isWindows(): boolean {
|
||||
return (
|
||||
process.platform === 'win32' ||
|
||||
/^(msys|cygwin)$/.test(process.env.OSTYPE || '')
|
||||
)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
const isWindows = require('./is-windows')
|
||||
|
||||
const pathLikeEnvVarWhitelist = new Set(['PATH', 'NODE_PATH'])
|
||||
|
||||
module.exports = varValueConvert
|
||||
|
||||
/**
|
||||
* This will transform UNIX-style list values to Windows-style.
|
||||
* For example, the value of the $PATH variable "/usr/bin:/usr/local/bin:."
|
||||
* will become "/usr/bin;/usr/local/bin;." on Windows.
|
||||
* @param {String} varValue Original value of the env variable
|
||||
* @param {String} varName Original name of the env variable
|
||||
* @returns {String} Converted value
|
||||
*/
|
||||
function replaceListDelimiters(varValue, varName = '') {
|
||||
const targetSeparator = isWindows() ? ';' : ':'
|
||||
if (!pathLikeEnvVarWhitelist.has(varName)) {
|
||||
return varValue
|
||||
}
|
||||
|
||||
return varValue.replace(/(\\*):/g, (match, backslashes) => {
|
||||
if (backslashes.length % 2) {
|
||||
// Odd number of backslashes preceding it means it's escaped,
|
||||
// remove 1 backslash and return the rest as-is
|
||||
return match.substr(1)
|
||||
}
|
||||
return backslashes + targetSeparator
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This will attempt to resolve the value of any env variables that are inside
|
||||
* this string. For example, it will transform this:
|
||||
* cross-env FOO=$NODE_ENV BAR=\\$NODE_ENV echo $FOO $BAR
|
||||
* Into this:
|
||||
* FOO=development BAR=$NODE_ENV echo $FOO
|
||||
* (Or whatever value the variable NODE_ENV has)
|
||||
* Note that this function is only called with the right-side portion of the
|
||||
* env var assignment, so in that example, this function would transform
|
||||
* the string "$NODE_ENV" into "development"
|
||||
* @param {String} varValue Original value of the env variable
|
||||
* @returns {String} Converted value
|
||||
*/
|
||||
function resolveEnvVars(varValue) {
|
||||
const envUnixRegex = /(\\*)(\$(\w+)|\${(\w+)})/g // $my_var or ${my_var} or \$my_var
|
||||
return varValue.replace(
|
||||
envUnixRegex,
|
||||
(_, escapeChars, varNameWithDollarSign, varName, altVarName) => {
|
||||
// do not replace things preceded by a odd number of \
|
||||
if (escapeChars.length % 2 === 1) {
|
||||
return varNameWithDollarSign
|
||||
}
|
||||
return (
|
||||
escapeChars.substr(0, escapeChars.length / 2) +
|
||||
(process.env[varName || altVarName] || '')
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an environment variable value to be appropriate for the current OS.
|
||||
* @param {String} originalValue Original value of the env variable
|
||||
* @param {String} originalName Original name of the env variable
|
||||
* @returns {String} Converted value
|
||||
*/
|
||||
function varValueConvert(originalValue, originalName) {
|
||||
return resolveEnvVars(replaceListDelimiters(originalValue, originalName))
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { isWindows } from './is-windows.js'
|
||||
|
||||
const pathLikeEnvVarWhitelist = new Set(['PATH', 'NODE_PATH'])
|
||||
|
||||
/**
|
||||
* This will transform UNIX-style list values to Windows-style.
|
||||
* For example, the value of the $PATH variable "/usr/bin:/usr/local/bin:."
|
||||
* will become "/usr/bin;/usr/local/bin;." on Windows.
|
||||
* @param varValue Original value of the env variable
|
||||
* @param varName Original name of the env variable
|
||||
* @returns Converted value
|
||||
*/
|
||||
function replaceListDelimiters(varValue: string, varName = ''): string {
|
||||
const targetSeparator = isWindows() ? ';' : ':'
|
||||
if (!pathLikeEnvVarWhitelist.has(varName)) {
|
||||
return varValue
|
||||
}
|
||||
|
||||
return varValue.replace(/(\\*):/g, (match, backslashes) => {
|
||||
if (backslashes.length % 2) {
|
||||
// Odd number of backslashes preceding it means it's escaped,
|
||||
// remove 1 backslash and return the rest as-is
|
||||
return match.substring(1)
|
||||
}
|
||||
return backslashes + targetSeparator
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This will attempt to resolve the value of any env variables that are inside
|
||||
* this string. For example, it will transform this:
|
||||
* cross-env FOO=$NODE_ENV BAR=\\$NODE_ENV echo $FOO $BAR
|
||||
* Into this:
|
||||
* FOO=development BAR=$NODE_ENV echo $FOO
|
||||
* (Or whatever value the variable NODE_ENV has)
|
||||
* Note that this function is only called with the right-side portion of the
|
||||
* env var assignment, so in that example, this function would transform
|
||||
* the string "$NODE_ENV" into "development"
|
||||
* @param varValue Original value of the env variable
|
||||
* @returns Converted value
|
||||
*/
|
||||
function resolveEnvVars(varValue: string): string {
|
||||
const envUnixRegex = /(\\*)(\$(\w+)|\${(\w+)})/g // $my_var or ${my_var} or \$my_var
|
||||
return varValue.replace(
|
||||
envUnixRegex,
|
||||
(_, escapeChars, varNameWithDollarSign, varName, altVarName) => {
|
||||
// do not replace things preceded by a odd number of \
|
||||
if (escapeChars.length % 2 === 1) {
|
||||
return varNameWithDollarSign
|
||||
}
|
||||
return (
|
||||
escapeChars.substring(0, escapeChars.length / 2) +
|
||||
(process.env[varName || altVarName] || '')
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an environment variable value to be appropriate for the current OS.
|
||||
* @param originalValue Original value of the env variable
|
||||
* @param originalName Original name of the env variable
|
||||
* @returns Converted value
|
||||
*/
|
||||
export function varValueConvert(
|
||||
originalValue: string,
|
||||
originalName: string,
|
||||
): string {
|
||||
return resolveEnvVars(replaceListDelimiters(originalValue, originalName))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": ["@epic-web/config/typescript"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src/**/*", "**/*.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'**/*.d.ts',
|
||||
'**/*.config.*',
|
||||
'**/coverage/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user