Repository: google/zx
Branch: main
Commit: 98531fcf3455
Files: 168
Total size: 1.3 MB
Directory structure:
gitextract_tzwf6ncx/
├── .commitlintrc
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.yml
│ │ └── idea.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── SECURITY.md
│ ├── codeql/
│ │ └── codeql-config.yml
│ ├── pages/
│ │ └── index.html
│ └── workflows/
│ ├── codeql.yml
│ ├── dev-publish.yml
│ ├── docs.yml
│ ├── jsr-publish.yml
│ ├── osv.yml
│ ├── publish.yml
│ ├── test.yml
│ └── zizmor.yml
├── .gitignore
├── .node_version
├── .nycrc
├── .prettierignore
├── .size-limit.json
├── LICENSE
├── README.md
├── build/
│ ├── 3rd-party-licenses
│ ├── cli.cjs
│ ├── cli.d.ts
│ ├── cli.js
│ ├── core.cjs
│ ├── core.d.ts
│ ├── core.js
│ ├── deno.js
│ ├── deps.cjs
│ ├── deps.d.ts
│ ├── error.d.ts
│ ├── esblib.cjs
│ ├── globals.cjs
│ ├── globals.d.ts
│ ├── globals.js
│ ├── goods.d.ts
│ ├── index.cjs
│ ├── index.d.ts
│ ├── index.js
│ ├── internals.cjs
│ ├── internals.d.ts
│ ├── log.d.ts
│ ├── md.d.ts
│ ├── util.cjs
│ ├── util.d.ts
│ ├── vendor-core.cjs
│ ├── vendor-core.d.ts
│ ├── vendor-extra.cjs
│ ├── vendor-extra.d.ts
│ ├── vendor.cjs
│ ├── vendor.d.ts
│ └── versions.d.ts
├── dcr/
│ └── Dockerfile
├── docs/
│ ├── .gitignore
│ ├── .vitepress/
│ │ ├── config.mts
│ │ └── theme/
│ │ ├── MyLayout.vue
│ │ ├── MyOxygen.vue
│ │ ├── custom.css
│ │ └── index.js
│ ├── api.md
│ ├── architecture.md
│ ├── cli.md
│ ├── configuration.md
│ ├── contribution.md
│ ├── faq.md
│ ├── getting-started.md
│ ├── index.md
│ ├── known-issues.md
│ ├── lite.md
│ ├── markdown.md
│ ├── migration-from-v7.md
│ ├── process-output.md
│ ├── process-promise.md
│ ├── quotes.md
│ ├── setup.md
│ ├── shell.md
│ ├── typescript.md
│ └── versions.md
├── examples/
│ ├── background-process.mjs
│ ├── backup-github.mjs
│ ├── fetch-weather.mjs
│ ├── hello.mjs
│ ├── interactive.mjs
│ └── parallel.mjs
├── lefthook.yml
├── man/
│ └── zx.1
├── package.json
├── scripts/
│ ├── build-clean.mjs
│ ├── build-dts.mjs
│ ├── build-js.mjs
│ ├── build-jsr.mjs
│ ├── build-pkgjson-lite.mjs
│ ├── build-pkgjson-main.mjs
│ ├── build-tests.mjs
│ ├── build-versions.mjs
│ ├── deno.polyfill.js
│ └── import-meta-url.polyfill.js
├── src/
│ ├── cli.ts
│ ├── core.ts
│ ├── deps.ts
│ ├── error.ts
│ ├── globals-jsr.ts
│ ├── globals.ts
│ ├── goods.ts
│ ├── index.ts
│ ├── internals.ts
│ ├── log.ts
│ ├── md.ts
│ ├── repl.ts
│ ├── util.ts
│ ├── vendor-core.ts
│ ├── vendor-extra.ts
│ ├── vendor.ts
│ └── versions.ts
├── test/
│ ├── all.test.js
│ ├── bench/
│ │ └── buf-join.mjs
│ ├── cli.test.js
│ ├── core.test.js
│ ├── deps.test.js
│ ├── error.test.ts
│ ├── export.test.js
│ ├── extra.test.js
│ ├── fixtures/
│ │ ├── argv.mjs
│ │ ├── copyright.txt
│ │ ├── echo.http
│ │ ├── exit-code.mjs
│ │ ├── filename-dirname.mjs
│ │ ├── interactive.mjs
│ │ ├── js-project/
│ │ │ ├── package.json
│ │ │ └── script.js
│ │ ├── markdown-crlf.md
│ │ ├── markdown.md
│ │ ├── md.http
│ │ ├── no-extension
│ │ ├── no-extension.mjs
│ │ ├── non-std-ext.zx
│ │ ├── require.mjs
│ │ ├── server.mjs
│ │ └── ts-project/
│ │ ├── package.json
│ │ ├── script.ts
│ │ └── tsconfig.json
│ ├── global.test.js
│ ├── goods.test.ts
│ ├── index.test.js
│ ├── it/
│ │ ├── build-dcr.test.js
│ │ ├── build-jsr.test.js
│ │ └── build-npm.test.js
│ ├── log.test.ts
│ ├── md.test.ts
│ ├── smoke/
│ │ ├── bun.test.js
│ │ ├── deno.test.js
│ │ ├── node.test.cjs
│ │ ├── node.test.mjs
│ │ ├── ts.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.test.json
│ │ └── win32.test.js
│ ├── util.test.js
│ └── vendor.test.js
├── test-d/
│ ├── core.test-d.ts
│ ├── globals.test-d.ts
│ └── goods.test-d.ts
├── tsconfig.json
└── zizmor.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .commitlintrc
================================================
{
"extends": [
"@commitlint/config-conventional"
],
"rules": {
"type-enum": [
2,
"always",
[
"build",
"ci",
"chore",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test"
]
],
}
}
================================================
FILE: .gitattributes
================================================
test/fixtures/markdown-crlf.md eol=crlf
build/** linguist-language=txt
================================================
FILE: .github/FUNDING.yml
================================================
github: antonmedv
================================================
FILE: .github/ISSUE_TEMPLATE/bug.yml
================================================
name: Bug Report
description: File a bug report.
title: '[Bug]: '
labels: ['bug']
assignees:
- antongolub
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
placeholder: Tell us what you see.
value: 'A bug happened!'
validations:
required: true
- type: textarea
id: what-expected
attributes:
label: How it should work?
description: Also tell us, what did you expect to happen?
value: "Here's how it should work..."
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: How to reproduce the bug?
description: Show an example.
value: 'Step-by-step instructions to reproduce the behavior. Code snippet, gist or issue-demo repository are helpful'
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What zx version are you running?
placeholder: e.g. 0.0.0
validations:
required: true
- type: dropdown
id: os
attributes:
label: What's OS kind?
multiple: true
options:
- Linux
- Mac
- Windows
- type: dropdown
id: runtime
attributes:
label: What JS runtime is used?
multiple: true
options:
- Node.js
- Deno
- Bun
- GraalVM
- type: input
id: runtime-version
attributes:
label: Runtime Version
description: What JS runtime version are you running?
placeholder: e.g. 0.0.0
- type: textarea
id: logs
attributes:
label: Error stack / relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/google/zx?tab=coc-ov-file).
options:
- label: I agree to follow this project's Code of Conduct
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/idea.yml
================================================
name: Feature Request
description: Idea, feature request or proposal.
title: '[Idea]: '
labels: ['feature']
assignees:
- antonmedv
body:
- type: markdown
attributes:
value: |
Thanks for sharing your vision!
- type: textarea
id: idea
attributes:
label: What's your idea?
placeholder: Tell us what you'd like to add or improve.
value: 'A new shiny feature!'
validations:
required: true
- type: textarea
id: why
attributes:
label: Why is that needed? How it may be useful?
placeholder: What problem does it solve?.
value: 'It will make something easier because...'
validations:
required: true
- type: textarea
id: demo
attributes:
label: Maybe you have a demo or example?
value: 'API sketch, code snippet'
render: ts
validations:
required: false
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Fixes #issue / suggests an improvement
```ts
import {$} from 'zx'
```
- [ ] **Setup** Set the latest Node.js LTS version.
- [ ] **Build**: I’ve run `npm build` before committing and verified the bundle updates correctly.
- [ ] **Tests**: I’ve `run test` and confirmed all tests succeed. Added tests to cover my changes if needed.
- [ ] **Docs**: I’ve added or updated relevant documentation as needed.
- [ ] **Sign** Commits have [verified signatures](https://docs.github.com/en/enterprise-cloud@latest/authentication/managing-commit-signature-verification/about-commit-signature-verification) and follow [conventinal commits spec](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] **CoC**: My changes follow [the project’s coding guidelines and Code of Conduct](https://github.com/google/zx?tab=coc-ov-file).
- [ ] **Review**: This PR represents original work and is not solely generated by AI tools.
================================================
FILE: .github/SECURITY.md
================================================
# Security Policy
## Supported Versions
| Version | Status | Comment |
|---------|--------------------|-----------------------------------------------------------------------|
| 8.x | :white_check_mark: | |
| 7.x | :warning: | Bugs, vulnerabilities, compatibility enhancements, performance issues |
| 6.x | :warning: | Critical bugs and vulnerability fixes |
| < 6.0 | :x: | **No longer supported**, please consider upgrade options |
## Reporting a Vulnerability
Please use https://g.co/vulnz to report security vulnerabilities.
We use https://g.co/vulnz for our intake and triage. For valid issues we will do coordination and disclosure here on
GitHub (including using a GitHub Security Advisory when necessary).
The Google Security Team will process your report within a day, and respond within a week (although it will depend on the severity of your report).
================================================
FILE: .github/codeql/codeql-config.yml
================================================
paths:
- .github
- docs
- examples
- man
- src
- scripts
- test
- test-d
paths-ignore:
- build
================================================
FILE: .github/pages/index.html
================================================
Here be dragons
================================================
FILE: .github/workflows/codeql.yml
================================================
name: 'CodeQL Advanced'
on:
push:
branches: ['main']
pull_request:
branches: ['main']
schedule:
- cron: '28 6 * * 3'
permissions: {}
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: 60
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
config-file: ./.github/codeql/codeql-config.yml
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: '/language:${{matrix.language}}'
================================================
FILE: .github/workflows/dev-publish.yml
================================================
name: Dev Publish
on:
workflow_dispatch:
permissions: {}
env:
npm_config_audit: false
npm_config_fund: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- run: npm ci
- run: npm test
env:
FORCE_COLOR: 3
- uses: actions/upload-artifact@v6
with:
name: build-${{ github.run_id }}
path: |
build
jsr.json
package.json
package-lite.json
package-main.json
retention-days: 1
version:
runs-on: ubuntu-latest
outputs:
v: ${{ steps.ref.outputs.ZX_VERSION }}
lite: ${{ steps.ref.outputs.ZX_VERSION }}-lite
dev: ${{ steps.ref.outputs.ZX_VERSION }}-dev.${{ steps.ref.outputs.SHA_SHORT }}
lite-dev: ${{ steps.ref.outputs.ZX_VERSION }}-lite-dev.${{ steps.ref.outputs.SHA_SHORT }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- id: ref
run: |
echo SHA_SHORT=$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
echo ZX_VERSION=$(jq -r '.version' package.json) >> $GITHUB_OUTPUT
npm-publish:
needs: [build, version]
runs-on: ubuntu-latest
permissions:
checks: read
statuses: write
contents: write
packages: write
id-token: write
env:
GOOGLE_NPM_REGISTRY: wombat-dressing-room.appspot.com
GOOGLE_NPM_TOKEN: ${{ secrets.AUTH_TOKEN }}
GH_NPM_REGISTRY: npm.pkg.github.com
GH_NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ZX_VERSION: ${{ needs.version.outputs.v }}
ZX_DEV_VERSION: ${{ needs.version.outputs.dev }}
ZX_LITE_DEV_VERSION: ${{ needs.version.outputs.lite-dev }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- name: Configure npmrc
run: |
echo "//${{ env.GOOGLE_NPM_REGISTRY }}/:_authToken=$GOOGLE_NPM_TOKEN" >> .npmrc
echo "//${{ env.GH_NPM_REGISTRY }}/:_authToken=$GH_NPM_TOKEN" >> .npmrc
- uses: actions/download-artifact@v7
with:
name: build-${{ github.run_id }}
- name: pushing lite snapshot to ${{ env.GOOGLE_NPM_REGISTRY }}
run: |
mv -f package-lite.json package.json
cat <<< $(jq '.version="${ZX_LITE_DEV_VERSION}"' package.json) > package.json
npm publish --provenance --access=public --no-git-tag-version --tag dev --registry https://${{ env.GOOGLE_NPM_REGISTRY }}
- name: pushing to ${{ env.GOOGLE_NPM_REGISTRY }}
run: |
mv -f package-main.json package.json
cat <<< $(jq '.version="${ZX_DEV_VERSION}"' package.json) > package.json
npm publish --provenance --access=public --no-git-tag-version --tag dev --registry https://${{ env.GOOGLE_NPM_REGISTRY }}
- name: pushing to ${{ env.GH_NPM_REGISTRY }}
run: |
cat <<< $(jq '.name="@${{ github.repository }}"' package.json) > package.json
npm publish --no-git-tag-version --access=public --tag dev --registry https://${{ env.GH_NPM_REGISTRY }}
jsr-publish:
needs: [build, version]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
ZX_DEV_VERSION: ${{ needs.version.outputs.dev }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- uses: actions/download-artifact@v7
with:
name: build-${{ github.run_id }}
- name: pushing to jsr.io
run: |
cat <<< $(jq '.version="${ZX_DEV_VERSION}"' jsr.json) > jsr.json
npx jsr publish --allow-dirty
# https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images
docker-publish:
needs: [build, version]
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
attestations: write
id-token: write
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
ZX_DEV_VERSION: ${{ needs.version.outputs.dev }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/download-artifact@v7
with:
name: build-${{ github.run_id }}
# Uses the `docker/login-action` action to log in to the Container registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=semver,pattern={{version}},value=v${{ env.ZX_DEV_VERSION }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
with:
context: ./
file: ./dcr/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
================================================
FILE: .github/workflows/docs.yml
================================================
name: Deploy docs
on:
workflow_dispatch:
release:
types: [created]
concurrency:
group: 'pages'
cancel-in-progress: false
permissions: {}
env:
npm_config_audit: false
npm_config_fund: false
npm_config_save: false
npm_config_package_lock: false
jobs:
deploy:
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: main
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Install deps
run: npm ci
- name: Add additional deps
run: npm i @rollup/rollup-linux-x64-gnu@4.46.4
- name: Build docs
run: npm run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v4.0.0
with:
path: 'docs/build'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/jsr-publish.yml
================================================
name: JSR Manual Publish
on:
workflow_dispatch:
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- run: npm ci
- run: npm test
env:
FORCE_COLOR: 3
- uses: actions/upload-artifact@v6
with:
name: build-${{ github.run_id }}
path: |
build
jsr.json
retention-days: 1
jsr-publish:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- uses: actions/download-artifact@v7
with:
name: build-${{ github.run_id }}
- name: Get zx version info
run: |
echo SHA_SHORT=$(git rev-parse --short HEAD) >> $GITHUB_ENV
echo ZX_VERSION=$(jq -r '.version' jsr.json) >> $GITHUB_ENV
- name: pushing to jsr.io
run: |
cat <<< $(jq '.version="${ZX_VERSION}-dev.${SHA_SHORT}"' jsr.json) > jsr.json
npx jsr publish --allow-dirty
================================================
FILE: .github/workflows/osv.yml
================================================
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# A sample workflow which sets up periodic OSV-Scanner scanning for vulnerabilities,
# in addition to a PR check which fails if new vulnerabilities are introduced.
#
# For more examples and options, including how to ignore specific vulnerabilities,
# see https://google.github.io/osv-scanner/github-action/
name: OSV-Scanner
permissions: {}
on:
pull_request:
branches: ['main']
merge_group:
branches: ['main']
schedule:
- cron: '45 6 * * 5'
push:
branches: ['main']
jobs:
scan-scheduled:
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
permissions:
security-events: write
contents: read
actions: read
uses: 'google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730' # v2.2.3
with:
# Example of specifying custom arguments
scan-args: |-
-r
./
scan-pr:
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
permissions:
security-events: write
contents: read
actions: read
uses: 'google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@e92b5d07338d4f0ba0981dffed17c48976ca4730' # v2.2.3
with:
# Example of specifying custom arguments
scan-args: |-
-r
./
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish
on:
workflow_dispatch:
release:
types: [created]
permissions: {}
env:
npm_config_audit: false
npm_config_fund: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- name: Compare release tag with package.json
if: github.event_name == 'release'
run: |
RELEASE_VERSION=${GITHUB_REF#refs/tags/}
PKG_VERSION=$(node -p "require('./package.json').version")
echo "Release tag: $RELEASE_VERSION"
echo "package.json: $PKG_VERSION"
[ "$RELEASE_VERSION" = "$PKG_VERSION" ] || { echo "❌ Mismatch"; exit 1; }
- run: npm ci
- run: npm test
env:
FORCE_COLOR: 3
- uses: actions/upload-artifact@v6
with:
name: build-${{ github.run_id }}
path: |
build
jsr.json
package.json
package-lite.json
package-main.json
retention-days: 1
version:
runs-on: ubuntu-latest
outputs:
v: ${{ steps.ref.outputs.ZX_VERSION }}
lite: ${{ steps.ref.outputs.ZX_VERSION }}-lite
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- id: ref
run: |
echo SHA_SHORT=$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
echo ZX_VERSION=$(jq -r '.version' package.json) >> $GITHUB_OUTPUT
npm-publish:
needs: [build, version]
runs-on: ubuntu-latest
permissions:
checks: read
statuses: write
contents: write
packages: write
id-token: write
env:
GOOGLE_NPM_REGISTRY: wombat-dressing-room.appspot.com
GOOGLE_NPM_TOKEN: ${{ secrets.AUTH_TOKEN }}
GH_NPM_REGISTRY: npm.pkg.github.com
GH_NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ZX_VERSION: ${{ needs.version.outputs.v }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- name: Configure npmrc
run: |
echo "//${{ env.GOOGLE_NPM_REGISTRY }}/:_authToken=$GOOGLE_NPM_TOKEN" >> .npmrc
echo "//${{ env.GH_NPM_REGISTRY }}/:_authToken=$GH_NPM_TOKEN" >> .npmrc
- uses: actions/download-artifact@v7
with:
name: build-${{ github.run_id }}
- name: pushing to ${{ env.GOOGLE_NPM_REGISTRY }}
run: |
mv -f package-main.json package.json
npm publish --provenance --access=public --registry https://${{ env.GOOGLE_NPM_REGISTRY }}
- name: pushing to ${{ env.GH_NPM_REGISTRY }}
run: |
cat <<< $(jq '.name="@${{ github.repository }}"' package.json) > package.json
npm publish --no-git-tag-version --access=public --registry https://${{ env.GH_NPM_REGISTRY }}
- name: pushing lite snapshot to ${{ env.GOOGLE_NPM_REGISTRY }}
run: |
mv -f package-lite.json package.json
npm publish --provenance --access=public --no-git-tag-version --tag lite --registry https://${{ env.GOOGLE_NPM_REGISTRY }}
jsr-publish:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- uses: actions/download-artifact@v7
with:
name: build-${{ github.run_id }}
- name: pushing to jsr.io
run: npx jsr publish --allow-dirty
docker-publish:
needs: [build, version]
runs-on: ubuntu-latest
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
ZX_VERSION: ${{ needs.version.outputs.v }}
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/download-artifact@v7
with:
name: build-${{ github.run_id }}
- name: Log in to the Container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=semver,pattern={{version}},value=v${{ env.ZX_VERSION }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
with:
context: ./
file: ./dcr/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
push:
pull_request:
schedule:
- cron: '0 12 */4 * *'
permissions:
contents: read
env:
npm_config_audit: false
npm_config_fund: false
npm_config_save: false
npm_config_package_lock: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Use Node.js 24
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- run: npm ci
- run: |
npm run build
cd build && ls -l
- uses: actions/upload-artifact@v6
with:
name: build
path: |
build
jsr.json
package.json
package-lite.json
package-main.json
retention-days: 1
checks:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: ${{ github.event_name == 'pull_request' && '15' || '1' }} # to ensure we have enough history for commitlint
- name: Use Node.js 24
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- uses: actions/download-artifact@v7
with:
name: build
- run: npm ci
- name: Format
run: npm run fmt:check
- name: License
run: npm run test:license
- name: Size
run: npm run test:size
- name: Dep audit
run: npm run test:audit
- name: Circular
run: npm run test:circular
- name: Bundles
run: npm run test:npm
timeout-minutes: 1
- name: JSR dry-run
run: npm run test:jsr
- name: Conventional Commits
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: npx commitlint --from "$BASE_SHA" --to "$HEAD_SHA" --verbose
test:
needs: build
runs-on: ubuntu-latest
env:
FORCE_COLOR: 3
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Use Node.js 24
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- uses: actions/download-artifact@v7
with:
name: build
- run: npm ci
- name: Unit tests
run: npm run test:coverage
timeout-minutes: 1
- name: Type tests
run: npm run test:types
docker-test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/download-artifact@v7
with:
name: build
- run: |
npm run build:dcr
npm run test:dcr
smoke-win32-node16:
strategy:
matrix:
os: [windows-2022, windows-2025]
name: smoke-${{ matrix.os }}-node16
runs-on: ${{ matrix.os }}
needs: build
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Use Node.js 16
uses: actions/setup-node@v6
with:
node-version: 16
cache: 'npm'
- uses: actions/download-artifact@v7
with:
name: build
- run: npm run test:smoke:win32
timeout-minutes: 2
env:
FORCE_COLOR: 3
smoke-bun:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup Bun
uses: antongolub/action-setup-bun@f0b9f339a7ce9ba1174a58484e4dc9bbd6f7b133 # v1.13.2
- uses: actions/download-artifact@v7
with:
name: build
- run: |
bun test ./test/smoke/bun.test.js
bun ./test/smoke/ts.test.ts
timeout-minutes: 1
env:
FORCE_COLOR: 3
smoke-deno:
runs-on: ubuntu-latest
needs: build
name: smoke-deno${{ matrix.deno-version }}
strategy:
matrix:
deno-version: [1, 2]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup Deno
uses: denoland/setup-deno@909cc5acb0fdd60627fb858598759246509fa755 # v2.0.2
with:
deno-version: ${{ matrix.deno-version }}
- run: deno install npm:types/node npm:types/fs-extra
- uses: actions/download-artifact@v7
with:
name: build
- run: deno test --allow-read --allow-sys --allow-env --allow-run ./test/smoke/deno.test.js
timeout-minutes: 1
env:
FORCE_COLOR: 3
smoke-node:
runs-on: ubuntu-latest
needs: build
name: smoke-node${{ matrix.node-version }}
strategy:
matrix:
node-version: [12, 14, 16, 18, 20, 22, 24, 25-nightly]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- uses: actions/download-artifact@v7
with:
name: build
- name: cjs smoke test
run: npm run test:smoke:cjs
- name: mjs smoke test
run: npm run test:smoke:mjs
- name: strip-types
if: matrix.node-version >= 22
run: npm run test:smoke:strip-types
smoke-graal:
needs: build
runs-on: ubuntu-latest
name: smoke-graal${{ matrix.version }}
strategy:
matrix:
version: [17, 20]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: graalvm/setup-graalvm@54b4f5a65c1a84b2fdfdc2078fe43df32819e4b1 # v1.4.4
with:
java-version: ${{ matrix.version }}
distribution: 'graalvm-community'
components: 'nodejs'
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v7
with:
name: build
- name: smoke tests
run: |
which node
node -v
npm run test:smoke:cjs
smoke-ts:
runs-on: ubuntu-latest
needs: build
name: smoke-ts${{ matrix.ts }}
strategy:
matrix:
ts: [4, 5, rc, next]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Use Node.js 24
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- name: Install deps
run: npm ci
- name: Install TypeScript ${{ matrix.ts }}
run: npm i --force typescript@${{ matrix.ts }}
- name: Override @types/node
if: matrix.ts == 4
run: npm i --force @types/node@24.2.0
- uses: actions/download-artifact@v7
with:
name: build
- name: tsc
run: npm run test:smoke:tsc
- name: tsx
run: npm run test:smoke:tsx
- name: ts-node
run: npm run test:smoke:ts-node
================================================
FILE: .github/workflows/zizmor.yml
================================================
name: Zizmor
on:
push:
branches: ['main']
pull_request:
branches: ['**']
permissions: {}
jobs:
zizmor:
name: zizmor
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b #v7.2.0
with:
enable-cache: false
- name: Run zizmor
run: uvx zizmor@1.22.0 .github/workflows -v -p --min-severity=medium
================================================
FILE: .gitignore
================================================
node_modules/
coverage/
package/
reports/
docs/.vitepress/cache/
yarn.lock
pnpm-lock.yaml
temp
test/fixtures/ts-project/build/
jsr.json
.npmrc
package-lite.json
package-main.json
================================================
FILE: .node_version
================================================
24
================================================
FILE: .nycrc
================================================
{
"reporter": ["html", "text"],
"lines": 98,
"branches": "90",
"statements": "98",
"exclude": [
"build/deno.js",
"build/vendor-extra.cjs",
"build/vendor-core.cjs",
"build/esblib.cjs",
"test/**",
"scripts",
"src/util.ts",
"src/core.ts",
"src/index.ts",
"src/vendor-extra.ts"
]
}
================================================
FILE: .prettierignore
================================================
node_modules/
build/
coverage/
package/
reports/
package-lock.json
yarn.lock
*.md
================================================
FILE: .size-limit.json
================================================
[
{
"name": "zx-lite",
"path": [
"build/3rd-party-licenses",
"build/core.cjs",
"build/core.d.ts",
"build/core.js",
"build/deno.js",
"build/error.d.ts",
"build/esblib.cjs",
"build/internals.cjs",
"build/internals.d.ts",
"build/log.d.ts",
"build/util.cjs",
"build/util.d.ts",
"build/vendor-core.cjs",
"build/vendor-core.d.ts",
"README.md",
"LICENSE"
],
"limit": "128.85 kB",
"brotli": false,
"gzip": false
},
{
"name": "js parts",
"path": [
"build/*.cjs",
"build/cli.js",
"build/core.js",
"build/index.js",
"build/globals.js",
"build/deno.js"
],
"limit": "850.20 kB",
"brotli": false,
"gzip": false
},
{
"name": "libdefs",
"path": "build/*.d.ts",
"limit": "44.55 kB",
"brotli": false,
"gzip": false
},
{
"name": "vendor",
"path": "build/vendor-*.{cjs,d.ts}",
"limit": "803.10 kB",
"brotli": false,
"gzip": false
},
{
"name": "all",
"path": [
"build/3rd-party-licenses",
"build/*.cjs",
"build/*.d.ts",
"build/cli.js",
"build/core.js",
"build/index.js",
"build/globals.js",
"build/deno.js",
"man/*",
"README.md",
"LICENSE"
],
"limit": "911.85 kB",
"brotli": false,
"gzip": false
}
]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
zx
```js
#!/usr/bin/env zx
await $`cat package.json | grep name`
const branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
const name = 'foo bar'
await $`mkdir /tmp/${name}`
```
Bash is great, but when it comes to writing more complex scripts,
many people prefer a more convenient programming language.
JavaScript is a perfect choice, but the Node.js standard library
requires additional hassle before using. No compromise, take the best of both. The `zx` package provides
useful cross-platform wrappers around `child_process`, escapes arguments and
gives sensible defaults.
## Install
```bash
npm install zx
```
All setup options: [zx/setup](https://google.github.io/zx/setup).
See also [**zx@lite**](https://google.github.io/zx/lite).
## Usage
* [Documentation at google.github.io/zx/](https://google.github.io/zx/)
* [Code examples](https://github.com/google/zx/tree/main/examples)
## Compatibility
* Linux, macOS, or Windows
* JavaScript Runtime:
* Node.js >= 12.17.0
* Bun >= 1.0.0
* Deno 1.x, 2.x
* GraalVM Node.js
* Some kind of [bash or PowerShell](https://google.github.io/zx/shell)
* [Both CJS or ESM](https://google.github.io/zx/setup#hybrid) modules in [JS or TS](https://google.github.io/zx/typescript)
## See also
- 🔥 [crow.watch](https://crow.watch) — a computing-focused community, link aggregation and discussion, [join](http://crow.watch/join/zx).
## License
[Apache-2.0](LICENSE)
Disclaimer: _This is not an officially supported Google product._
================================================
FILE: build/3rd-party-licenses
================================================
THIRD PARTY LICENSES
@nodelib/fs.scandir@2.1.5
https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.scandir
MIT
@nodelib/fs.stat@2.0.5
https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.stat
MIT
@nodelib/fs.walk@1.2.8
https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.walk
MIT
@sindresorhus/merge-streams@4.0.0
Sindre Sorhus
sindresorhus/merge-streams
MIT
@webpod/ps@1.0.0
git://github.com/webpod/ps.git
MIT
braces@3.0.3
Jon Schlinkert (https://github.com/jonschlinkert)
micromatch/braces
MIT
chalk@5.6.2
chalk/chalk
MIT
create-require@1.1.1
Maël Nison , Paul Soporan , Pooya Parsa
nuxt-contrib/create-require
MIT
envapi@0.2.3
Anton Golub
git+https://github.com/webpod/envapi.git
MIT
fast-glob@3.3.3
Denis Malinochkin
mrmlnc/fast-glob
MIT
fastq@1.20.1
Matteo Collina
git+https://github.com/mcollina/fastq.git
ISC
fill-range@7.1.1
Jon Schlinkert (https://github.com/jonschlinkert)
jonschlinkert/fill-range
MIT
fs-extra@11.3.3
JP Richardson
https://github.com/jprichardson/node-fs-extra
MIT
glob-parent@5.1.2
Gulp Team (https://gulpjs.com/)
gulpjs/glob-parent
ISC
globby@16.1.1
Sindre Sorhus
sindresorhus/globby
MIT
graceful-fs@4.2.11
https://github.com/isaacs/node-graceful-fs
ISC
is-glob@4.0.3
Jon Schlinkert (https://github.com/jonschlinkert)
micromatch/is-glob
MIT
is-path-inside@4.0.0
Sindre Sorhus
sindresorhus/is-path-inside
MIT
isexe@4.0.0
Isaac Z. Schlueter (http://blog.izs.me/)
https://github.com/isaacs/isexe
BlueOak-1.0.0
jsonfile@6.2.0
JP Richardson
git@github.com:jprichardson/node-jsonfile.git
MIT
maml.js@0.0.3
Anton Medvedev
git+https://github.com/maml-dev/maml.js.git
MIT
merge2@1.4.1
git@github.com:teambition/merge2.git
MIT
micromatch@4.0.8
Jon Schlinkert (https://github.com/jonschlinkert)
micromatch/micromatch
MIT
node-fetch-native@1.6.7
unjs/node-fetch-native
MIT
picomatch@2.3.1
Jon Schlinkert (https://github.com/jonschlinkert)
micromatch/picomatch
MIT
run-parallel@1.2.0
Feross Aboukhadijeh
git://github.com/feross/run-parallel.git
MIT
to-regex-range@5.0.1
Jon Schlinkert (https://github.com/jonschlinkert)
micromatch/to-regex-range
MIT
unicorn-magic@0.4.0
Sindre Sorhus
sindresorhus/unicorn-magic
MIT
which@6.0.1
GitHub Inc.
git+https://github.com/npm/node-which.git
ISC
yaml@2.8.2
Eemeli Aro
github:eemeli/yaml
ISC
zurk@0.11.10
Anton Golub
git+https://github.com/webpod/zurk.git
MIT
================================================
FILE: build/cli.cjs
================================================
#!/usr/bin/env node
"use strict";
const {
__export,
__toESM,
__toCommonJS,
__async
} = require('./esblib.cjs');
const import_meta_url =
typeof document === 'undefined'
? new (require('url').URL)('file:' + __filename).href
: (document.currentScript && document.currentScript.src) ||
new URL('main.js', document.baseURI).href
// src/cli.ts
var cli_exports = {};
__export(cli_exports, {
argv: () => argv,
autorun: () => autorun,
injectGlobalRequire: () => injectGlobalRequire,
isMain: () => isMain,
main: () => main,
normalizeExt: () => normalizeExt,
printUsage: () => printUsage,
transformMarkdown: () => transformMarkdown
});
module.exports = __toCommonJS(cli_exports);
var import_node_url = __toESM(require("url"), 1);
var import_node_process2 = __toESM(require("process"), 1);
var import_index = require("./index.cjs");
var import_deps = require("./deps.cjs");
// src/repl.ts
var import_node_process = __toESM(require("process"), 1);
var import_node_repl = __toESM(require("repl"), 1);
var import_node_util = require("util");
var import_core = require("./core.cjs");
var _a;
var HISTORY = (_a = import_node_process.default.env.ZX_REPL_HISTORY) != null ? _a : import_core.path.join(import_core.os.homedir(), ".zx_repl_history");
function startRepl() {
return __async(this, arguments, function* (history = HISTORY) {
import_core.defaults.verbose = false;
const r = import_node_repl.default.start({
prompt: import_core.chalk.greenBright.bold("\u276F "),
useGlobal: true,
preview: false,
writer(output) {
return output instanceof import_core.ProcessOutput ? output.toString().trimEnd() : (0, import_node_util.inspect)(output, { colors: true });
}
});
r.setupHistory(history, () => {
});
});
}
// src/cli.ts
var import_util2 = require("./util.cjs");
// src/md.ts
var import_util = require("./util.cjs");
function transformMarkdown(buf) {
var _a2;
const out = [];
const tabRe = /^( +|\t)/;
const fenceRe = new RegExp("^(? {0,3})(?(`{3,20}|~{3,20}))(?:(?js|javascript|ts|typescript)|(?sh|shell|bash)|.*)$");
let state = "root";
let prevEmpty = true;
let fenceChar = "";
let stripRe = null;
let endRe = /^$/;
let linePrefix = "";
let closeOut = "";
const isEnd = (s) => fenceChar !== "" && endRe.test(s);
for (const line of (0, import_util.bufToString)(buf).split(/\r\n|[\n\r\u2028\u2029]/)) {
switch (state) {
case "root": {
const g = (_a2 = line.match(fenceRe)) == null ? void 0 : _a2.groups;
if (g == null ? void 0 : g.fence) {
fenceChar = g.fence[0];
stripRe = g.indent ? new RegExp(`^ {0,${g.indent.length}}`) : null;
endRe = new RegExp(`^ {0,3}${fenceChar}{${g.fence.length},}[ \\t]*$`);
if (g.js) {
out.push("");
linePrefix = "";
closeOut = "";
} else if (g.bash) {
out.push("await $`");
linePrefix = "";
closeOut = "`";
} else {
out.push("");
linePrefix = "// ";
closeOut = "";
}
state = "fence";
prevEmpty = false;
break;
}
if (prevEmpty && tabRe.test(line)) {
out.push(line);
state = "tab";
continue;
}
prevEmpty = line === "";
out.push("// " + line);
continue;
}
case "tab":
if (line === "") out.push("");
else if (tabRe.test(line)) out.push(line);
else {
out.push("// " + line);
state = "root";
}
prevEmpty = line === "";
break;
case "fence":
if (isEnd(line)) {
out.push(closeOut);
state = "root";
prevEmpty = true;
fenceChar = "";
} else {
const s = stripRe ? line.replace(stripRe, "") : line;
out.push(linePrefix + s);
prevEmpty = false;
}
break;
}
}
return out.join("\n");
}
// src/cli.ts
var import_vendor = require("./vendor.cjs");
var import_meta = {};
var EXT = ".mjs";
var EXT_RE = /^\.[mc]?[jt]sx?$/;
var argv = (0, import_index.parseArgv)(import_node_process2.default.argv.slice(2), {
default: (0, import_index.resolveDefaults)({ ["prefer-local"]: false }, "ZX_", import_node_process2.default.env, /* @__PURE__ */ new Set(["env", "install", "registry"])),
// exclude 'prefer-local' to let minimist infer the type
string: ["shell", "prefix", "postfix", "eval", "cwd", "ext", "registry", "env"],
boolean: ["version", "help", "quiet", "verbose", "install", "repl", "experimental"],
alias: { e: "eval", i: "install", v: "version", h: "help", l: "prefer-local", "env-file": "env" },
stopEarly: true,
parseBoolean: true,
camelCase: true
});
autorun(import_meta);
function autorun(meta) {
if (meta && isMain(meta))
main().catch((err) => {
if (err instanceof import_index.ProcessOutput) {
console.error("Error:", err.message);
} else {
console.error(err);
}
import_node_process2.default.exitCode = 1;
});
}
function printUsage() {
console.log(`
${import_index.chalk.bold("zx " + import_index.VERSION)}
A tool for writing better scripts
${import_index.chalk.bold("Usage")}
zx [options]
================================================
FILE: docs/.vitepress/theme/MyOxygen.vue
================================================
================================================
FILE: docs/.vitepress/theme/custom.css
================================================
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#f11a7b 10%,
#feffac
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
rgba(241, 26, 123, 0.33) 50%,
rgba(254, 255, 172, 0.33) 50%
);
--vp-home-hero-image-filter: blur(40px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(72px);
}
}
================================================
FILE: docs/.vitepress/theme/index.js
================================================
import DefaultTheme from 'vitepress/theme'
import MyLayout from './MyLayout.vue'
import './custom.css'
export default {
...DefaultTheme,
// override the Layout with a wrapper component that
// injects the slots
Layout: MyLayout,
}
================================================
FILE: docs/api.md
================================================
# API Reference
## `$.sync`
Zx provides both synchronous and asynchronous command executions, returns [`ProcessOutput`](./process-output) or [`ProcessPromise`](./process-promise) respectively.
```js
const list = await $`ls -la`
const dir = $.sync`pwd`
```
## `$({...})`
`$` object holds the default zx [configuration](./configuration), which is used for all execution. To specify a custom preset use `$` as factory:
```js
const $$ = $({
verbose: false,
env: {NODE_ENV: 'production'},
})
const env = await $$`node -e 'console.log(process.env.NODE_ENV)'`
const pwd = $$.sync`pwd`
const hello = $({quiet: true})`echo "Hello!"`
```
Moreover, presets are chainable:
```js
const $1 = $({ nothrow: true })
assert.equal((await $1`exit 1`).exitCode, 1)
const $2 = $1({ sync: true }) // Both {nothrow: true, sync: true} are applied
assert.equal($2`exit 2`.exitCode, 2)
const $3 = $({ sync: true })({ nothrow: true })
assert.equal($3`exit 3`.exitCode, 3)
```
### `$({input})`
The input option passes the specified `stdin` to the command.
```js
const p1 = $({ input: 'foo' })`cat`
const p2 = $({ input: Readable.from('bar') })`cat`
const p3 = $({ input: Buffer.from('baz') })`cat`
const p4 = $({ input: p3 })`cat`
const p5 = $({ input: await p3 })`cat`
```
### `$({signal})`
The signal option makes the process abortable.
```js
const {signal} = new AbortController()
const p = $({ signal })`sleep 9999`
setTimeout(() => signal.abort('reason'), 1000)
```
### `$({timeout})`
The timeout option makes the process autokillable after the specified delay.
```js
const p = $({timeout: '1s'})`sleep 999`
```
### `$({nothrow})`
The `nothrow` option suppresses errors and returns a `ProcessOutput` with details.
```js
const o1 = await $({nothrow: true})`exit 1`
o1.ok // false
o1.exitCode // 1
o1.message // exit code: 1 ...
const o2 = await $({nothrow: true, spawn() { throw new Error('BrokenSpawn') }})`echo foo`
o2.ok // false
o2.exitCode // null
o2.message // BrokenSpawn ...
```
The full options list:
```ts
interface Options {
cwd: string
ac: AbortController
signal: AbortSignal
input: string | Buffer | Readable | ProcessOutput | ProcessPromise
timeout: Duration
timeoutSignal: NodeJS.Signals
stdio: StdioOptions
verbose: boolean
sync: boolean
env: NodeJS.ProcessEnv
shell: string | true
nothrow: boolean
prefix: string
postfix: string
quote: typeof quote
quiet: boolean
detached: boolean
preferLocal: boolean | string | string[]
spawn: typeof spawn
spawnSync: typeof spawnSync
store: TSpawnStore
log: typeof log
kill: typeof kill
killSignal: NodeJS.Signals
halt: boolean
delimiter: string | RegExp
}
```
See also [Configuration](./configuration).
## `cd()`
Changes the current working directory.
```js
cd('/tmp')
await $`pwd` // => /tmp
```
Like `echo`, in addition to `string` arguments, `cd` accepts and trims
trailing newlines from `ProcessOutput` enabling common idioms like:
```js
cd(await $`mktemp -d`)
```
> ⚠️ `cd` invokes `process.chdir()` internally, so it does affect the global context. To keep `process.cwd()` in sync with separate `$` calls enable [syncProcessCwd()](#syncprocesscwd) hook.
## `fetch()`
A wrapper around the [node-fetch-native](https://www.npmjs.com/package/node-fetch-native)
package.
```js
const r1 = await fetch('https://example.com')
const json = await r1.json()
const r2 = await fetch('https://example.com', {
signal: AbortSignal.timeout(5000),
})
```
For some cases, `text()` or `json()` can produce extremely large output that exceeds the string size limit.
Streams are just for that, so we've attached a minor adjustment to the `fetch` API to make it more pipe friendly.
```js
const p1 = fetch('https://example.com').pipe($`cat`)
const p2 = fetch('https://example.com').pipe`cat`
```
## `question()`
A wrapper around the [readline](https://nodejs.org/api/readline.html) API.
```js
const bear = await question('What kind of bear is best? ')
const selected = await question('Select an option:', {
choices: ['A', 'B', 'C'],
})
```
## `sleep()`
A wrapper around the `setTimeout` function.
```js
await sleep(1000)
```
## `echo()`
A `console.log()` alternative which can take [ProcessOutput](#processoutput).
```js
const branch = await $`git branch --show-current`
echo`Current branch is ${branch}.`
// or
echo('Current branch is', branch)
```
## `stdin()`
Returns the stdin as a string.
```js
const content = JSON.parse(await stdin())
```
## `within()`
Creates a new async context.
```js
await $`pwd` // => /home/path
$.foo = 'bar'
within(async () => {
$.cwd = '/tmp'
$.foo = 'baz'
setTimeout(async () => {
await $`pwd` // => /tmp
$.foo // baz
}, 1000)
})
await $`pwd` // => /home/path
$.foo // still 'bar'
```
```js
await $`node --version` // => v20.2.0
const version = await within(async () => {
$.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; nvm use 16;'
return $`node --version`
})
echo(version) // => v16.20.0
```
## `syncProcessCwd()`
Keeps the `process.cwd()` in sync with the internal `$` current working directory if it is changed via [cd()](#cd).
```ts
import {syncProcessCwd} from 'zx'
syncProcessCwd()
syncProcessCwd(false) // pass false to disable the hook
```
> This feature is disabled by default because of performance overhead.
## `retry()`
Retries a callback for a few times. Will return the first
successful result, or will throw after the specified attempts count.
```js
const p = await retry(10, () => $`curl https://medv.io`)
// With a specified delay between attempts.
const p = await retry(20, '1s', () => $`curl https://medv.io`)
// With an exponential backoff.
const p = await retry(30, expBackoff(), () => $`curl https://medv.io`)
```
## `spinner()`
Starts a simple CLI spinner.
```js
await spinner(() => $`long-running command`)
// With a message.
await spinner('working...', () => $`sleep 99`)
```
And it's disabled for `CI` by default.
## `glob()`
The [globby](https://github.com/sindresorhus/globby) package.
```js
const packages = await glob(['package.json', 'packages/*/package.json'])
const markdowns = glob.sync('*.md') // sync API shortcut
```
## `which()`
The [which](https://github.com/npm/node-which) package.
```js
const node = await which('node')
```
If nothrow option is used, returns null if not found.
```js
const pathOrNull = await which('node', { nothrow: true })
```
## `ps`
The [@webpod/ps](https://github.com/webpod/ps) package to provide a cross-platform way to list processes.
```js
const all = await ps.lookup()
const nodejs = await ps.lookup({ command: 'node' })
const children = await ps.tree({ pid: 123 })
const fulltree = await ps.tree({ pid: 123, recursive: true })
```
## `kill()`
A process killer.
```js
await kill(123)
await kill(123, 'SIGKILL')
```
## `tmpdir()`
Creates a temporary directory.
```js
t1 = tmpdir() // /os/based/tmp/zx-1ra1iofojgg/
t2 = tmpdir('foo') // /os/based/tmp/zx-1ra1iofojgg/foo/
```
## `tmpfile()`
Temp file factory.
```js
f1 = tmpfile() // /os/based/tmp/zx-1ra1iofojgg
f2 = tmpfile('f2.txt') // /os/based/tmp/zx-1ra1iofojgg/foo.txt
f3 = tmpfile('f3.txt', 'string or buffer')
f4 = tmpfile('f4.sh', 'echo "foo"', 0o744) // executable
```
## `minimist`
The [minimist](https://www.npmjs.com/package/minimist) package.
```js
const argv = minimist(process.argv.slice(2), {})
```
## `argv`
A minimist-parsed version of the `process.argv` as `argv`.
```js
if (argv.someFlag) {
echo('yes')
}
```
Use minimist options to customize the parsing:
```js
const myCustomArgv = minimist(process.argv.slice(2), {
boolean: [
'force',
'help',
],
alias: {
h: 'help',
},
})
```
## `chalk`
The [chalk](https://www.npmjs.com/package/chalk) package.
```js
console.log(chalk.blue('Hello world!'))
```
## `fs`
The [fs-extra](https://www.npmjs.com/package/fs-extra) package.
```js
const {version} = await fs.readJson('./package.json')
```
## `os`
The [os](https://nodejs.org/api/os.html) package.
```js
await $`cd ${os.homedir()} && mkdir example`
```
## `path`
The [path](https://nodejs.org/api/path.html) package.
```js
await $`mkdir ${path.join(basedir, 'output')}`
```
## `YAML`
The [yaml](https://www.npmjs.com/package/yaml) package.
```js
console.log(YAML.parse('foo: bar').foo)
```
## `MAML`
The [maml.js](https://www.npmjs.com/package/maml.js) package.
```js
const maml = `{
example: "MAML"
# Comments are supported
notes: """
This is a multiline string.
Keeps formatting as‑is.
"""
}`
console.log(MAML.parse(maml).example) // MAML
```
## `dotenv`
The [envapi](https://www.npmjs.com/package/envapi) package.
An API to interact with environment vars in [dotenv](https://www.npmjs.com/package/dotenv) format.
```js
// parse
const raw = 'FOO=BAR\nBAZ=QUX'
const data = dotenv.parse(raw) // {FOO: 'BAR', BAZ: 'QUX'}
await fs.writeFile('.env', raw)
// load
const env = dotenv.load('.env')
await $({ env })`echo $FOO`.stdout // BAR
// config
dotenv.config('.env')
process.env.FOO // BAR
```
## `versions`
Exports versions of the zx dependencies.
```ts
import { versions } from 'zx'
versions.zx // 8.7.2
versions.chalk // 5.4.1
```
## `quote()`
Default bash quoting function.
```js
quote("$FOO") // "$'$FOO'"
```
## `quotePowerShell()`
PowerShell specific quoting.
```js
quotePowerShell("$FOO") // "'$FOO'"
```
## `useBash()`
Enables bash preset: sets `$.shell` to `bash` and `$.quote` to `quote`.
```js
useBash()
```
## `usePowerShell()`
Switches to PowerShell. Applies the `quotePowerShell` for quoting.
```js
usePowerShell()
```
## `usePwsh()`
Sets pwsh (PowerShell v7+) as `$.shell` default.
```js
usePwsh()
```
================================================
FILE: docs/architecture.md
================================================
# The zx architecture
This section helps to better understand the `zx` concepts and logic, and will be useful for those who want to become a project contributor, make tools based on it, or create something similar from scratch.
## High-level modules
| Module | Description |
|-------------------------------------------------------------------------|---------------------------------------------------------------------|
| [zurk](https://github.com/webpod/zurk) | Execution engine for spawning and managing child processes. |
| [./src/core.ts](https://github.com/google/zx/blob/main/src/core.ts) | `$` factory, presets, utilities, high-level APIs. |
| [./src/goods.ts](https://github.com/google/zx/blob/main/src/goods.ts) | Utilities for common tasks like fs ops, glob search, fetching, etc. |
| [./src/cli.ts](https://github.com/google/zx/blob/main/src/cli.ts) | CLI interface and scripts pre-processors. |
| [./src/deps.ts](https://github.com/google/zx/blob/main/src/deps.ts) | Dependency analyzing and installation. |
| [./src/vendor.ts](https://github.com/google/zx/blob/main/src/vendor.ts) | Third-party libraries. |
| [./src/utils.ts](https://github.com/google/zx/blob/main/src/utils.ts) | Generic helpers. |
| [./src/md.ts](https://github.com/google/zx/blob/main/src/md.ts) | Markdown scripts extractor. |
| [./src/error.ts](https://github.com/google/zx/blob/main/src/error.ts) | Error handling and formatting. |
| [./src/global.ts](https://github.com/google/zx/blob/main/src/global.ts) | Global injectors. |
## Core design
### `Options`
A set of options for `$` and `ProcessPromise` configuration. `defaults` holds the initial library preset. `Snapshot` captures the current `Options `context and attaches isolated subparts.
### `$`
A piece of template literal magic.
```ts
interface Shell<
S = false,
R = S extends true ? ProcessOutput : ProcessPromise,
> {
(pieces: TemplateStringsArray, ...args: any[]): R
= Partial, R = O extends { sync: true } ? Shell : Shell>(opts: O): R
sync: {
(pieces: TemplateStringsArray, ...args: any[]): ProcessOutput
(opts: Partial>): Shell
}
}
$`cmd ${arg}` // ProcessPromise
$(opts)`cmd ${arg}` // ProcessPromise
$.sync`cmd ${arg}` // ProcessOutput
$.sync(opts)`cmd ${arg}` // ProcessOutput
```
The `$` factory creates `ProcessPromise` instances and bounds with snapshot-context via `Proxy` and `AsyncLocalStorage`. The trick:
```ts
const storage = new AsyncLocalStorage()
const getStore = () => storage.getStore() || defaults
function within(callback: () => R): R {
return storage.run({ ...getStore() }, callback)
}
// Inside $ factory ...
const opts = getStore()
if (!Array.isArray(pieces)) {
return function (this: any, ...args: any) {
return within(() => Object.assign($, opts, pieces).apply(this, args))
}
}
```
### `ProcessPromise`
A promise-inherited class represents and operates a child process, provides methods for piping, killing, response formatting.
#### Lifecycle
| Stage | Description |
|--------------|------------------------|
| `initial` | Blank instance |
| `halted` | Awaits running |
| `running` | Process in action |
| `fulfilled` | Successfully completed |
| `rejected` | Failed |
| Gear | Description |
|--------------|---------------------------------------------------------------------------------------------|
| `build()` | Produces `cmd` from template and context, applies `quote` to arguments |
| `run()` | Spawns the process and captures its data via `zurk` events listeners |
| `finalize()` | Assigns the result to the instance: analyzes status code, invokes `_resolve()`, `_reject()` |
#### Piping
The remarkable part is `pipe()` and `_pipe()` interactions: the first provides a facade, the second binds different streams with acceptors. We use initialization inside static scope to comply with TS method visibility restrictions and to avoid extra `Proxy` usage:
```ts
const p = $`cmd`
const crits = await p.pipe.stderr`grep critical`
const names = await p.pipe.stdout`grep name`
```
Another `pipe()` superpower is an internal recorder. It allows binding processes at any stage w/o data loss, even if settled.
```ts
const onData = (chunk: string | Buffer) => from.write(chunk)
const fill = () => {
for (const chunk of source) from.write(chunk)
}
ee.once(source, () => {
fill() // 1. Pulling previous records
ee.on(source, onData) // 2. Listening for new data
}).once('end', () => {
ee.removeListener(source, onData)
from.end()
})
```
Wayback machine in action:
```ts
const p = $`cmd`
await p
await p.pipe`grep name` // Still works, but `p` is settled
```
### `ProcessOutput`
A class that represents the output of a `ProcessPromise`. It provides methods to access the process's stdout, stderr, exit code and extra methods for formatting the output and checking the process's success.
### `Fail`
Consolidates error handling functionality across the zx library: errors codes mapping, formatting, stack parsing.
## CLI
zx provides CLI with embedded script preprocessor to construct an execution context (apply presets, inject global vars) and to install the required deps. Then runs the specified script.
| Helper | Description |
|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `main()` | Initializes a preset from flags, env vars and pushes the reader. |
| `readScript()` | Fetches, parses and transforms the specified source into a runnable form. `stdin` reader, `https` loader and `md` transformer act right here. Deps analyzer internally relies on [depseek](https://www.npmjs.com/package/depseek) and inherits its limitations |
| `runScript()` | Executes the script in the target context via async `import()`, handles temp assets after. |
## Building
In the early stages of the project, we [had some difficulties](https://dev.to/antongolub/how-and-why-do-we-bundle-zx-1ca6) with zx packaging. We couldn't find a suitable tool for assembly, so we made our own toolkit based on [esbuild](https://github.com/evanw/esbuild) and [dts-bundle-generator](https://github.com/timocov/dts-bundle-generator). This process is divided into several scripts.
| Script | Description |
|----------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`./scripts/build-dts.mjs`](https://github.com/google/zx/blob/main/scripts/build-dts.mjs) | Extracts and merges 3rd-party types, generates `dts` files. |
| [`./scripts/build-js.mjs`](https://github.com/google/zx/blob/main/scripts/build-js.mjs) | Produces [hybrid bundles](./setup#hybrid) for each package entry point |
| [`./scripts/build-jsr.mjs`](https://github.com/google/zx/blob/main/scripts/build-jsr.mjs) | Builds extra assets for [JSR](https://jsr.io/@webpod/zx) publishing |
| [`./scripts/build-tests.mjs`](https://github.com/google/zx/blob/main/scripts/build-test.mjs) | Generates autotests to verify exports consistency |
Corresponding tasks are defined in the `package.json.scripts`:
```json
{
"prebuild": "rm -rf build",
"build": "npm run build:js && npm run build:dts && npm run build:tests",
"build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry=src/*.ts:!src/error.ts:!src/repl.ts:!src/md.ts:!src/log.ts:!src/globals-jsr.ts:!src/goods.ts && npm run build:vendor",
"build:vendor": "node scripts/build-js.mjs --format=cjs --entry=src/vendor-*.ts --bundle=all --external='./internals.ts'",
"build:tests": "node scripts/build-tests.mjs",
"build:dts": "tsc --project tsconfig.json && rm build/repl.d.ts build/globals-jsr.d.ts && node scripts/build-dts.mjs",
"build:dcr": "docker build -f ./dcr/Dockerfile . -t zx",
"build:jsr": "node scripts/build-jsr.mjs"
}
```
## Testing
We understand the importance, impact and risks of the tool and invest significant efforts in comprehensive research of its quality, reliability and safety. Therefore, we use an extensive set of tools and testing scenarios.
First, we inspect not how the code was written, but how it actually works. Tests mainly focus on prod bundles, `pretest` ensures they are actual.
```json
{
"pretest": "npm run build"
}
```
A basic set of implementation correctness checks is provided by unit tests. We also evaluate coverage to ensure that areas of code are not missed.
```json
{
"test:unit": "node --experimental-transform-types ./test/all.test.js",
"test:coverage": "c8 -c .nycrc --check-coverage npm run test:unit"
}
```
Next, we control the contents of all the artifacts and examine their functionality.
```json
{
"test:npm": "node ./test/it/build-npm.test.js",
"test:jsr": "node ./test/it/build-jsr.test.js",
"test:dcr": "node ./test/it/build-dcr.test.js"
}
```
Bundle code duplication issues are highlighted through size check.
```json
{
"test:size": "size-limit"
}
```
Static analyzers are responsible for code quality.
```json
{
"fmt:check": "prettier --check .",
"test:circular": "madge --circular src/*"
}
```
Type checking examines declarations and compatibility with different loaders and transpilers.
```json
{
"test:types": "tsd",
"test:smoke:strip-types": "node --experimental-strip-types test/smoke/ts.test.ts",
"test:smoke:tsx": "tsx test/smoke/ts.test.ts",
"test:smoke:tsc": "cd test/smoke && mkdir -p node_modules && ln -s ../../../ ./node_modules/zx; ../../node_modules/typescript/bin/tsc -v && ../../node_modules/typescript/bin/tsc --esModuleInterop --module node16 --rootDir . --outdir ./temp ts.test.ts && node ./temp/ts.test.js",
"test:smoke:ts-node": "cd test/smoke && node --loader ts-node/esm ts.test.ts"
}
```
We also check compatibility with all the target [runtimes x OS variants](https://github.com/google/zx/blob/main/.github/workflows/test.yml).
```json
{
"test:smoke:bun": "bun test ./test/smoke/bun.test.js && bun ./test/smoke/node.test.mjs",
"test:smoke:win32": "node ./test/smoke/win32.test.js",
"test:smoke:deno": "deno test ./test/smoke/deno.test.js --allow-read --allow-sys --allow-env --allow-run",
}
```
CJS and EMS exports are verified separately.
```json
{
"test:smoke:cjs": "node ./test/smoke/node.test.cjs",
"test:smoke:mjs": "node ./test/smoke/node.test.mjs"
}
```
Finally, we check the license and supply chain security issues.
```json
{
"test:license": "node ./test/extra.test.js",
"test:audit": "npm audit fix",
"test:workflow": "zizmor .github/workflows -v -p --min-severity=medium"
}
```
================================================
FILE: docs/cli.md
================================================
# CLI Usage
Zx provides a CLI for running scripts. It comes with the package and can be used as `zx` executable (if referenced in package.json `"scripts"`, installed [globally](/setup#install) or added to the `$PATH` somehow).
```sh
zx script.mjs
```
`npx` or `node` inits are valid too.
```sh
npx zx script.mjs
node -r zx/globals script.mjs
node --import zx/globals script.mjs
```
## No extensions
If the script does not have a file extension (like `.git/hooks/pre-commit`), zx
assumes that it is
an [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename)
module unless the `--ext` option is specified.
## Non-standard extension
`zx` internally loads scripts via `import` API, so you can use any extension supported by the runtime (nodejs, deno, bun) or apply a [custom loader](https://nodejs.org/api/cli.html#--experimental-loadermodule).
However, if the script has a non-js-like extension (`/^\.[mc]?[jt]sx?$/`) and the `--ext` is specified, it will be used.
```bash
zx script.zx # Unknown file extension "\.zx"
zx --ext=mjs script.zx # OK
```
## Markdown
The CLI supports [markdown](/markdown) files and interprets `ts`, `js` and `bash` code blocks as scripts.
```bash
zx docs/markdown.md
```
## Remote scripts
If the argument to the `zx` executable starts with `https://`, the file will be
downloaded and executed.
```bash
zx https://raw.githubusercontent.com/google/zx/refs/heads/main/examples/hello.mjs
```
> [!WARNING]
Make sure you trust the remote source and understand the code before running it.
## Scripts from stdin
The `zx` supports executing scripts from stdin.
```js
zx << 'EOF'
await $`pwd`
EOF
```
## `--eval`
Evaluate the following argument as a script.
```bash
cat package.json | zx --eval 'const v = JSON.parse(await stdin()).version; echo(v)'
```
## `--repl`
Starts zx in [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) mode.
## `--install`
```js
// script.mjs:
import sh from 'tinysh'
sh.say('Hello, world!')
```
Add `--install` flag to the `zx` command to install missing dependencies
automatically.
```bash
zx --install script.mjs
```
You can also specify needed version by adding comment with `@` after
the import.
```js
import sh from 'tinysh' // @^1
```
## `--registry`
By default, `zx` uses `https://registry.npmjs.org` as a registry. Customize if needed.
```bash
zx --registry=https://registry.yarnpkg.com script.mjs
```
## `--quiet`
Suppress any outputs.
## `--verbose`
Enable verbose mode.
## `--shell`
Specify a custom shell binary path. By default, zx refers to `bash`.
```bash
zx --shell=/bin/another/sh script.mjs
```
## `--prefer-local, -l`
Prefer locally installed packages and binaries.
```bash
zx --prefer-local=/external/node_modules/or/nm-root script.mjs
```
## `--prefix & --postfix`
Attach a command to the beginning or the end of every command.
```bash
zx --prefix='echo foo;' --postfix='; echo bar' script.mjs
```
## `--cwd`
Set the current working directory.
```bash
zx --cwd=/foo/bar script.mjs
```
## `--env`
Specify an env file.
```bash
zx --env=/path/to/some.env script.mjs
```
When `cwd` option is specified, it will be used as base path:
`--cwd='/foo/bar' --env='../.env'` → `/foo/.env`
## `--ext`
Overrides the default script extension (`.mjs`).
## `--version, -v`
Print the current `zx` version.
## `--help, -h`
Print help notes.
## Environment variables
All the previously mentioned options can be set via the corresponding `ZX_`-prefixed environment variables.
```bash
ZX_VERBOSE=true ZX_SHELL='/bin/bash' zx script.mjs
```
```yaml
steps:
- name: Run script
run: zx script.mjs
env:
ZX_VERBOSE: true
ZX_SHELL: '/bin/bash'
```
## `__filename & __dirname`
In [ESM](https://nodejs.org/api/esm.html) modules, Node.js does not provide
`__filename` and `__dirname` globals. As such globals are really handy in scripts,
zx provides these for use in `.mjs` files (when using the `zx` executable).
## `require()`
In [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename)
modules, the `require()` function is not defined.
The `zx` provides `require()` function, so it can be used with imports in `.mjs`
files (when using `zx` executable).
```js
const {version} = require('./package.json')
```
================================================
FILE: docs/configuration.md
================================================
# Configuration
## `$.shell`
Specifies what shell is used. Default is `which bash`.
```js
$.shell = '/usr/bin/bash'
```
Or use a CLI argument: `--shell=/bin/bash`
## `$.spawn`
Specifies a `spawn` api. Defaults to native `child_process.spawn`.
To override a sync API implementation, set `$.spawnSync` correspondingly.
## `$.kill`
Specifies a `kill` function. The default implements _half-graceful shutdown_ via `ps.tree()`. You can override with more sophisticated logic.
```js
import treekill from 'tree-kill'
$.kill = (pid, signal = 'SIGTERM') => {
return new Promise((resolve, reject) => {
treekill(pid, signal, (err) => {
if (err) reject(err)
else resolve()
})
})
}
```
## `$.prefix`
Specifies the command that will be prefixed to all commands run.
Default is `set -euo pipefail;`.
Or use a CLI argument: `--prefix='set -e;'`
## `$.postfix`
Like a `$.prefix`, but for the end of the command.
```js
$.postfix = '; exit $LastExitCode' // for PowerShell compatibility
```
## `$.preferLocal`
Specifies whether to prefer `node_modules/.bin` located binaries over globally system installed ones.
```js
$.preferLocal = true
await $`c8 npm test`
```
You can also specify a directory to search for local binaries:
```js
$.preferLocal = '/some/to/bin'
$.preferLocal = ['/path/to/bin', '/another/path/bin']
```
## `$.quote`
Specifies a function for escaping special characters during
command substitution.
## `$.verbose`
Specifies verbosity. Default is `false`.
In verbose mode, `zx` prints all executed commands alongside with their
outputs.
Or use the CLI argument: `--verbose` to set `true`.
## `$.quiet`
Suppresses all output. Default is `false`.
Via CLI argument: `--quiet` sets `$.quiet = true`.
## `$.env`
Specifies an environment variables map.
Defaults to `process.env`.
## `$.cwd`
Specifies a current working directory of all processes created with the `$`.
The [cd()](#cd) func changes only `process.cwd()` and if no `$.cwd` specified,
all `$` processes use `process.cwd()` by default (same as `spawn` behavior).
## `$.log`
Specifies a [logging function](src/log.ts).
```ts
import {LogEntry, log} from 'zx/core'
$.log = (entry: LogEntry) => {
switch (entry.kind) {
case 'cmd':
// for example, apply custom data masker for cmd printing
process.stderr.write(masker(entry.cmd))
break
default:
log(entry)
}
}
```
The log mostly acts like a debugger, so by default it uses `process.error` for output.
Override the `$.log.output` to change the stream.
```ts
$.log.output = process.stdout
```
Define `$.log.formatters` to customize each log entry kind printing:
```ts
$.log.formatters = {
cmd: (entry: LogEntry) => `CMD: ${entry.cmd}`,
fetch: (entry: LogEntry) => `FETCH: ${entry.url}`
}
```
## `$.timeout`
Specifies a timeout for the command execution.
```js
$.timeout = '1s'
$.timeoutSignal= 'SIGKILL'
await $`sleep 999`
```
## `$.delimiter`
Specifies a delimiter for splitting command output into lines.
Defaults to `\r?\n` (newline or carriage return + newline).
```js
$.delimiter = /\0/ // null character
await $`find ./ -type f -print0 -maxdepth 1`
```
## `$.defaults`
Holds default configuration values. They will be used if the corresponding
`$` options are not specified.
```ts
$.defaults = {
cwd: process.cwd(),
env: process.env,
verbose: false,
quiet: false,
sync: false,
shell: true,
prefix: 'set -euo pipefail;', // for bash
postfix: '; exit $LastExitCode', // for powershell
nothrow: false,
stdio: 'pipe', // equivalent to ['pipe', 'pipe', 'pipe']
detached: false,
preferLocal: false,
spawn: childProcess.spawn,
spawnSync: childProcess.spawnSync,
log: $.log,
kill: $.kill,
killSignal: 'SIGTERM',
timeoutSignal: 'SIGTERM',
delimiter: /\r?\n/,
}
```
================================================
FILE: docs/contribution.md
================================================
# Contribution Guide
zx is a fully [open-source project](https://github.com/google/zx), which is developing by the community for the community.
We welcome contributions of any kind, including but not limited to:
* Bug reports
* Feature requests
* Code contributions
* Documentation improvements
* Discussions
https://google.github.io/zx/contribution
## Community Guidelines
This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/).
In short: all contributors are treated with respect and fairness.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## How to Contribute
Before proposing changes, look for similar ones in the project's [issues](https://github.com/google/zx/issues) and [pull requests](https://github.com/google/zx/pulls). If you can't decide, create a new [discussion](https://github.com/google/zx/discussions) topic, and we will help you figure it out. Dive also into [architecture notes](/architecture) to observe design concepts. When ready to move on:
* Prepare your development environment.
* Switch to the recommended version of Node.js
* Install manually `Node.js >= 22`.
* Delegate the routine to any version manager, that [supports .node_version config](https://stackoverflow.com/questions/27425852/what-uses-respects-the-node-version-file)
* Use [Volta](https://volta.sh/), the target version will be set automatically from the `package.json`
* Bash is essential for running zx scripts. Linux and macOS users usually have it installed by default. Consider using [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install) or [Git Bash](https://git-scm.com/downloads) if you are on Windows.
* Fork [the repository](https://github.com/google/zx).
* Create a new branch.
* Make your changes.
* If you are adding a new feature, please include additional tests. The coverage threshold is 98%.
* Create a [conventional-commits](https://www.conventionalcommits.org/en/v1.0.0/) compliant messages.
* Ensure that everything is working:
* `npm run fmt` to format your code.
* `npm run test:coverage` to run the tests.
* Push the changes to your fork.
* Create a pull request.
* Describe your changes in detail.
* Reference any related issues if applicable.
## Code Reviews
All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.
## License
The project is licensed under the [Apache-2.0](https://github.com/google/zx?tab=Apache-2.0-1-ov-file#readme)
================================================
FILE: docs/faq.md
================================================
# FAQ
## Passing env variables
```js
process.env.FOO = 'bar'
await $`echo $FOO`
```
## Passing array of values
When passing an array of values as an argument to `$`, items of the array will
be escaped
individually and concatenated via space.
Example:
```js
const files = [...]
await $`tar cz ${files}`
```
## Importing into other scripts
It is possible to make use of `$` and other functions via explicit imports:
```js
#!/usr/bin/env node
import {$} from 'zx'
await $`date`
```
## Attaching a profile
By default `child_process` does not include aliases and bash functions.
But you are still able to do it by hand. Just attach necessary directives
to the `$.prefix`.
```js
$.prefix += 'export NVM_DIR=$HOME/.nvm; source $NVM_DIR/nvm.sh; '
await $`nvm -v`
```
## Using GitHub Actions
The default GitHub Action runner comes with `npx` installed.
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# node-version: 22
- name: Build with zx
env:
FORCE_COLOR: 3
run: |
npx zx <<'EOF'
await $`...`
EOF
```
## Verbose and Quiet
zx has internal logger, which captures events if a condition is met:
| Event | Verbose | Quiet | Description |
|--------|---------|---------|------------------------------|
| stdout | `true` | `false` | Spawned process stdout |
| stderr | `any` | `false` | Process stderr data |
| cmd | `true` | `false` | Command execution |
| fetch | `true` | `false` | Fetch resources by http(s) |
| cd | `true` | `false` | Change directory |
| retry | `true` | `false` | Capture exec error |
| custom | `true` | `false` | User-defined event |
By default, both `$.verbose` and `$.quiet` options are `false`, so only `stderr` events are written. Any output goes to the `process.stderr` stream.
You may control this flow globally or in-place
```js
// Global debug mode on
$.verbose = true
await $`echo hello`
// Suppress the particular command
await $`echo fobar`.quiet()
// Suppress everything
$.quiet = true
await $`echo world`
// Turn on in-place debug
await $`echo foo`.verbose()
```
You can also override the default logger with your own:
```js
// globally
$.log = (entry) => {
switch (entry.kind) {
case 'cmd':
console.log('Command:', entry.cmd)
break
default:
console.warn(entry)
}
}
// or in-place
$({log: () => {}})`echo hello`
```
## Canary / Beta / RC builds
Impatient early adopters can try the experimental zx versions.
But keep in mind: these builds are ⚠️️__beta__ in every sense.
```bash
npm i zx@dev
npx zx@dev --install --quiet <<< 'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)'
```
================================================
FILE: docs/getting-started.md
================================================
# Getting Started
## Overview
```js
#!/usr/bin/env zx
await $`cat package.json | grep name`
const branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
const name = 'foo bar'
await $`mkdir /tmp/${name}`
```
Bash is great, but when it comes to writing more complex scripts,
many people prefer a more convenient programming language.
JavaScript is a perfect choice, but the Node.js standard library
requires additional hassle before using. The `zx` package provides
useful wrappers around `child_process`, escapes arguments and
gives sensible defaults.
## Install
```bash
npm install zx
```
or many [other ways](/setup)
## Usage
Write your scripts in a file with an `.mjs` extension in order to
use `await` at the top level. If you prefer the `.js` extension,
wrap your scripts in something like `void async function () {...}()`. [TypeScript](./typescript.md) is also supported.
Add the following shebang to the beginning of your `zx` scripts:
```bash
#!/usr/bin/env zx
```
Now you will be able to run your script like so:
```bash
chmod +x ./script.mjs
./script.mjs
```
Or via the [CLI](cli.md):
```bash
zx ./script.mjs
```
All functions (`$`, `cd`, `fetch`, etc) are available straight away
without any imports.
Or import globals explicitly (for better autocomplete in VS Code).
```js
import 'zx/globals'
```
### ``$`command` ``
Executes a given command using the `spawn` func
and returns [`ProcessPromise`](process-promise.md). It supports both sync and async modes.
```js
const list = await $`ls -la`
const dir = $.sync`pwd`
```
Everything passed through `${...}` will be automatically escaped and quoted.
```js
const name = 'foo & bar'
await $`mkdir ${name}`
```
**There is no need to add extra quotes.** Read more about it in
[quotes](quotes.md).
You can pass an array of arguments if needed:
```js
const flags = [
'--oneline',
'--decorate',
'--color',
]
await $`git log ${flags}`
```
In async mode, zx awaits any `thenable` in literal before executing the command.
```js
const a1 = $`echo foo`
const a2 = new Promise((resolve) => setTimeout(resolve, 20, ['bar', 'baz']))
await $`echo ${a1} ${a2}` // foo bar baz
```
If the executed program returns a non-zero exit code,
[`ProcessOutput`](#processoutput) will be thrown.
```js
try {
await $`exit 1`
} catch (p) {
console.log(`Exit code: ${p.exitCode}`)
console.log(`Error: ${p.stderr}`)
}
```
### `ProcessOutput`
```ts
class ProcessOutput {
readonly stdout: string
readonly stderr: string
readonly signal: string
readonly exitCode: number
// ...
toString(): string // Combined stdout & stderr.
valueOf(): string // Returns .toString().trim()
}
```
The output of the process is captured as-is. Usually, programs print a new
line `\n` at the end.
If `ProcessOutput` is used as an argument to some other `$` process,
**zx** will use stdout and trim the new line.
```js
const date = await $`date`
await $`echo Current date is ${date}.`
```
## License
[Apache-2.0](https://github.com/google/zx/blob/main/LICENSE)
Disclaimer: _This is not an officially supported Google product._
================================================
FILE: docs/index.md
================================================
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
titleTemplate: google/zx
hero:
name: "zx"
text: "A tool for writing better scripts"
image:
src: /img/logo.svg
alt: Zx Logo
actions:
- theme: brand
text: Documentation
link: /getting-started
features:
- title: Simple
details: Write your scripts in a familiar language.
- title: Powerful
details: Interact with the full ecosystem of JS libraries.
- title: Batteries included
details: Everything you need, right out of the box.
---
================================================
FILE: docs/known-issues.md
================================================
# Known Issues
## Output gets truncated
This is a known issue with `console.log()` (see [nodejs/node#6379](https://github.com/nodejs/node/issues/6379)).
It's caused by different behaviour of `console.log()` writing to the terminal vs
to a file. If a process calls `process.exit()`, buffered output will be truncated.
To prevent this, the process should use `process.exitCode = 1` and wait for the
process to exit itself. Or use something like [exit](https://www.npmjs.com/package/exit) package.
Workaround is to write to a temp file:
```js
const tmp = await $`mktemp` // Creates a temp file.
const {stdout} = await $`cmd > ${tmp}; cat ${tmp}`
```
## Colors in subprocess
You may see what tools invoked with `await $` don't show colors, compared to
what you see in a terminal. This is because, the subprocess does not think it's
a TTY and the subprocess turns off colors. Usually there is a way force
the subprocess to add colors.
```js
process.env.FORCE_COLOR='1'
await $`cmd`
```
================================================
FILE: docs/lite.md
================================================
# zx@lite
Just core functions without extras:
* ~7x smaller than the full version
* No CLI, no docs, no manpage assets embedded
* Same package name, but different publish channel — `@lite`
* Less code — ~~less risks~~ faster and more reliable ISEC audit
* Recommended for custom toolkits based on zx
```sh
npm i zx@lite
npm i zx@8.5.5-lite
```
Detailed comparison: [versions](./versions)
```js
import { $ } from 'zx'
await $`echo foo`
```
### Range of choice
**tool size ← [`child_process`](https://nodejs.org/api/child_process.html) [`zurk`](https://github.com/webpod/zurk) `zx@lite` `zx` → built-in functionality**
================================================
FILE: docs/markdown.md
================================================
# Markdown Scripts
Imagine a script with code blocks, formatted comments, schemas, illustrations, etc. [Markdown](https://en.wikipedia.org/wiki/Markdown) is right for this purpose.
Combine `ts`, `js`, `bash` sections to produce a single zx scenario. For example:
````text
# Some script
`ls` — is an unix command to get directory contents. Let's see how to use it in `zx`:
```js
// ts, js, cjs, mjs, etc
const {stdout} = await $`ls -l`
console.log('directory contents:', stdout)
```
This part invokes the same command in a different way:
```bash
# bash syntax
ls -l
```
````
And how it looks like:
> # Some script
> `ls` — is an unix command to get directory contents. Let's see how to use it in `zx`:
> ```js
> // ts, js, cjs, mjs, etc
> const {stdout} = await $`ls -l`
> console.log('directory contents:', stdout)
> ```
>
> This part invokes the same command in a different way:
> ```bash
> # bash syntax
> ls -l
> ```
The rest is simple: just run via `zx` command:
```bash
zx script.md
```
## Hints
You can use imports here as well:
```js
await import('chalk')
```
`js`, `javascript`, `ts`, `typescript`, `sh`, `shell`, `bash` code blocks will be executed by zx.
```bash
VAR=$(date)
echo "$VAR" | wc -c
```
Other kinds are ignored:
```css
body .hero {
margin: 42px;
}
```
The `__filename` will be pointed to **markdown.md**:
```js
console.log(chalk.yellowBright(__filename))
```
================================================
FILE: docs/migration-from-v7.md
================================================
# Migration from v7 to v8
[v8.0.0 release](https://github.com/google/zx/releases/tag/8.0.0) brought many features, improvements, optimizations and fixes, but also has introduced a few breaking changes. Fortunately, everything can be restored and legacy v7 scripts can still run with minor configurations.
1. `$.verbose` is set to `false` by default, but errors are still printed to `stderr`. Set `$.quiet = true` to suppress any output.
```js
$.verbose = true // everything works like in v7
$.quiet = true // to completely turn off logging
```
2. `ssh` API was dropped. Install [webpod](https://github.com/webpod/webpod) package instead.
```js
// import {ssh} from 'zx' ↓
import {ssh} from 'webpod'
const remote = ssh('user@host')
await remote`echo foo`
```
3. zx is not looking for `PowerShell` anymore, on Windows by default. If you still need it, use the `usePowerShell` helper to enable:
```js
import { usePowerShell, useBash } from 'zx'
usePowerShell() // to enable powershell
useBash() // switch to bash, the default
```
To look for modern [PowerShell v7+](https://github.com/google/zx/pull/790), invoke `usePwsh()` helper instead:
```js
import { usePwsh } from 'zx'
usePwsh()
```
4. Process cwd synchronization between `$` invocations is now disabled by default. This functionality is provided via an async hook and can now be controlled directly.
```js
import { syncProcessCwd } from 'zx'
syncProcessCwd() // restores legacy v7 behavior
```
# 🚀
Keep in mind, v7 is in maintenance mode, so it will not receive any new enhancements. We encourage you to upgrade to the latest: it's [16x smaller](https://dev.to/antongolub/how-and-why-do-we-bundle-zx-1ca6), faster, safer, more reliable and useful in a [wider range of practical scenarios](https://github.com/google/zx/releases).
================================================
FILE: docs/process-output.md
================================================
# Process Output
Represents a cmd execution result.
```ts
const p = $`command` // ProcessPromise
const o = await p // ProcessOutput
```
```ts
interface ProcessOutput extends Error {
// Exit code of the process: 0 for success, non-zero for failure
exitCode: number
// Signal that caused the process to exit: SIGTERM, SIGKILL, etc.
signal: NodeJS.Signals | null
// Holds the stdout of the process
stdout: string
// Process errors are written to stderr
stderr: string
buffer(): Buffer
json(): T
blob(type = 'text/plain'): Blob
text(encoding: Encoding = 'utf8'): string
// Output lines splitted by newline
lines(delimiter?: string | RegExp): string[]
// combined stdout and stderr
toString(): string
// Same as toString() but trimmed
valueOf(): string
}
```
================================================
FILE: docs/process-promise.md
================================================
# Process Promise
The `$` returns a `ProcessPromise` instance, which inherits native `Promise`. When resolved, it becomes a [`ProcessOutput`](./process-output.md).
```js
const p = $`command` // ProcessPromise
const o = await p // ProcessOutput
```
By default, `$` spawns a new process immediately, but you can delay the start to trigger in manually.
```ts
const p = $({halt: true})`command`
const o = await p.run()
```
## `stage`
Shows the current process stage: `initial` | `halted` | `running` | `fulfilled` | `rejected`
```ts
const p = $`echo foo`
p.stage // 'running'
await p
p.stage // 'fulfilled'
```
## `stdin`
Returns a writable stream of the stdin process. Accessing
this getter will trigger execution of a subprocess with [`stdio('pipe')`](#stdio).
Do not forget to end the stream.
```js
const p = $`while read; do echo $REPLY; done`
p.stdin.write('Hello, World!\n')
p.stdin.end()
```
By default, each process is created with stdin in _inherit_ mode.
## `stdout`/`stderr`
Returns a readable streams of stdout/stderr process.
```js
const p = $`npm init`
for await (const chunk of p.stdout) {
echo(chunk)
}
```
## `exitCode`
Returns a promise which resolves to the exit code of the process.
```js
if (await $`[[ -d path ]]`.exitCode == 0) {
// ...
}
```
## `json(), text(), lines(), buffer(), blob()`
Output formatters collection.
```js
const p = $`echo 'foo\nbar'`
await p.text() // foo\n\bar\n
await p.text('hex') // 666f6f0a0861720a
await p.buffer() // Buffer.from('foo\n\bar\n')
await p.lines() // ['foo', 'bar']
// You can specify a custom lines delimiter if necessary:
await $`touch foo bar baz; find ./ -type f -print0`
.lines('\0') // ['./bar', './baz', './foo']
// If the output is a valid JSON, parse it in place:
await $`echo '{"foo": "bar"}'`
.json() // {foo: 'bar'}
```
## `pid, cwd, cmd, fullCmd`
Process metadata getters.
```js
const p = $`sleep 1`
p.pid // process id
p.cwd // process working directory
p.cmd // command: "sleep 1"
p.fullCmd // full command with prefix and postfix: "set -euo pipefail;sleep 1"
```
## `[Symbol.asyncIterator]`
Returns an async iterator for the process stdout.
```js
const p = $`echo "Line1\nLine2\nLine3"`
for await (const line of p) {
console.log(line)
}
// Custom delimiter can be specified:
for await (const line of $({
delimiter: '\0'
})`touch foo bar baz; find ./ -type f -print0`) {
console.log(line)
}
```
## `pipe()`
Redirects the output of the process. Almost same as `|` in bash but with enhancements.
```js
const greeting = await $`printf "hello"`
.pipe($`awk '{printf $1", world!"}'`)
.pipe($`tr '[a-z]' '[A-Z]'`)
```
`pipe()` accepts any kind `Writable`, `ProcessPromise` or a file path.
```js
await $`echo "Hello, stdout!"`
.pipe(fs.createWriteStream('/tmp/output.txt'))
```
You can pass a string to `pipe()` to implicitly create a receiving file. The previous example is equivalent to:
```js
await $`echo "Hello, stdout!"`
.pipe('/tmp/output.txt')
```
Chained streams become _thenables_, so you can `await` them:
```js
const p = $`echo "hello"`
.pipe(getUpperCaseTransform())
.pipe(fs.createWriteStream(tempfile())) // <- stream
const o = await p
```
And the `ProcessPromise` itself is compatible with the standard `Stream.pipe` API:
```js
const { stdout } = await fs
.createReadStream(await fs.writeFile(file, 'test'))
.pipe(getUpperCaseTransform())
.pipe($`cat`)
```
Pipes can be used to show a real-time output of the process:
```js
await $`echo 1; sleep 1; echo 2; sleep 1; echo 3;`
.pipe(process.stdout)
```
And the time machine is in stock! You can pipe the process at any phase: on start, in the middle, or even after the end. All chunks will be buffered and processed in the right order.
```js
const result = $`echo 1; sleep 1; echo 2; sleep 1; echo 3`
const piped1 = result.pipe`cat`
let piped2
setTimeout(() => { piped2 = result.pipe`cat` }, 1500)
(await piped1).toString() // '1\n2\n3\n'
(await piped2).toString() // '1\n2\n3\n'
```
This mechanism allows you to easily split streams to multiple consumers:
```js
const p = $`some-command`
const [o1, o2] = await Process.all([
p.pipe`log`,
p.pipe`extract`
])
```
Use combinations of `pipe()` and [`nothrow()`](#nothrow):
```js
await $`find ./examples -type f -print0`
.pipe($`xargs -0 grep ${'missing' + 'part'}`.nothrow())
.pipe($`wc -l`)
```
And literals! The `pipe()` does support them too:
```js
await $`printf "hello"`
.pipe`awk '{printf $1", world!"}'`
.pipe`tr '[a-z]' '[A-Z]'`
```
The `pipe()` allows not only chain or split stream, but also to merge them.
```js
const $h = $({ halt: true })
const p1 = $`echo foo`
const p2 = $h`echo a && sleep 0.1 && echo c && sleep 0.2 && echo e`
const p3 = $h`sleep 0.05 && echo b && sleep 0.1 && echo d`
const p4 = $`sleep 0.4 && echo bar`
const p5 = $h`cat`
await p1
p1.pipe(p5)
p2.pipe(p5)
p3.pipe(p5)
p4.pipe(p5)
const { stdout } = await p5.run() // 'foo\na\nb\nc\nd\ne\nbar\n'
```
By default, `pipe()` operates with `stdout` stream, but you can specify `stderr` as well:
```js
const p = $`echo foo >&2; echo bar`
const o1 = (await p.pipe.stderr`cat`).toString() // 'foo\n'
const o2 = (await p.pipe.stdout`cat`).toString() // 'bar\n'
```
The [signal](/api#signal) option, if specified, will be transmitted through the pipeline.
```js
const ac = new AbortController()
const { signal } = ac
const p = $({ signal, nothrow: true })`echo test`.pipe`sleep 999`
setTimeout(() => ac.abort(), 50)
try {
await p
} catch ({ message }) {
message // The operation was aborted
}
```
In short, combine anything you want:
```js
const getUpperCaseTransform = () => new Transform({
transform(chunk, encoding, callback) {
callback(null, String(chunk).toUpperCase())
},
})
// $ > stream (promisified) > $
const o1 = await $`echo "hello"`
.pipe(getUpperCaseTransform())
.pipe($`cat`)
o1.stdout // 'HELLO\n'
// stream > $
const file = tempfile()
await fs.writeFile(file, 'test')
const o2 = await fs
.createReadStream(file)
.pipe(getUpperCaseTransform())
.pipe($`cat`)
o2.stdout // 'TEST'
```
## `unpipe()`
Opposite of `pipe()`, it removes the process from the pipeline.
```js
const p1 = $`echo foo && sleep 0.05 && echo bar && sleep 0.05 && echo baz && sleep 0.05 && echo qux`
const p2 = $`echo 1 && sleep 0.05 && echo 2 && sleep 0.05 && echo 3`
const p3 = $`cat`
p1.pipe(p3)
p2.pipe(p3)
setTimeout(() => p1.unpipe(p3), 105)
assert.equal((await p1).stdout, 'foo\nbar\nbaz\nqux')
assert.equal((await p2).stdout, '1\n2\n3')
assert.equal((await p3).stdout, 'foo\n1\nbar\n2\n3')
```
## `kill()`
Kills the process and all children.
By default, signal `SIGTERM` is sent. You can specify a signal via an argument.
```js
const p = $`sleep 999`
setTimeout(() => p.kill('SIGINT'), 100)
await p
```
Killing the expired process raises an error:
```js
const p = await $`sleep 999`
p.kill() // Error: Too late to kill the process.
```
## `abort()`
Terminates the process via an `AbortController` signal.
```js
const ac = new AbortController()
const {signal} = ac
const p = $({signal})`sleep 999`
setTimeout(() => ac.abort('reason'), 100)
await p
```
If `ac` or `signal` is not provided, it will be autocreated and could be used to control external processes.
```js
const p = $`sleep 999`
const {signal} = p
const res = fetch('https://example.com', {signal})
p.abort('reason')
```
The process may be aborted while executing, the method raises an error otherwise:
```js
const p = $({nothrow: true})`sleep 999`
p.abort() // ok
await p
p.abort() // Error: Too late to abort the process.
```
## `stdio()`
Specifies a standard input-output for the process.
```js
const h$ = $({halt: true})
const p1 = h$`read`.stdio('inherit', 'pipe', null).run()
const p2 = h$`read`.stdio('pipe').run() // sets ['pipe', 'pipe', 'pipe']
```
Keep in mind, `stdio` should be set before the process is started, so the preset syntax might be preferable:
```js
await $({stdio: ['pipe', 'pipe', 'pipe']})`read`
```
## `nothrow()`
Changes behavior of `$` to not throw an exception on non-zero exit codes. Equivalent to [`$({nothrow: true})` option](./api#nothrow).
```js
await $`grep something from-file`.nothrow()
// Inside a pipe():
await $`find ./examples -type f -print0`
.pipe($`xargs -0 grep something`.nothrow())
.pipe($`wc -l`)
// Accepts a flag to switch nothrow mode for the specific command
$.nothrow = true
await $`echo foo`.nothrow(false)
```
If only the `exitCode` is needed, you can use [`exitCode`](#exitcode) directly:
```js
if (await $`[[ -d path ]]`.exitCode == 0) {
//...
}
// Equivalent of:
if ((await $`[[ -d path ]]`.nothrow()).exitCode == 0) {
//...
}
```
## `quiet()`
Changes behavior of `$` to enable suppress mode.
```js
// Command output will not be displayed.
await $`grep something from-file`.quiet()
$.quiet = true
await $`echo foo`.quiet(false) // Disable for the specific command
```
## `verbose()`
Enables verbose output. Pass `false` to disable.
```js
await $`grep something from-file`.verbose()
$.verbose = true
await $`echo foo`.verbose(false) // Turn off verbose mode once
```
## `timeout()`
Kills the process after a specified period.
```js
await $`sleep 999`.timeout('5s')
// Or with a specific signal.
await $`sleep 999`.timeout('5s', 'SIGKILL')
```
If the process is already settled, the method does nothing. Passing nullish value will disable the timeout.
================================================
FILE: docs/quotes.md
================================================
# Quotes
Bash supports various ways to quote arguments: single quotes, double quotes, and a bash-specific method using C-style
quotes `$'...'`. Zx prefers the latter approach.
```js
const name = 'foo & bar'
await $`mkdir ${name}`
```
> [!WARNING]
> Zx automatically escapes and quotes anything within `${...}`, so there's no need for additional quotes. Moreover, this may result in an **unsafe injection**.
> ```ts
> const args = ['param && echo bar']
> const p = $`echo --foo=$'${args}'`
> (await p).stdout // '--foo=$param\nbar\n'
> ```
The following examples produce the same, correct result:
```js
await $`mkdir ${'path/to-dir/' + name}`
```
```js
await $`mkdir path/to-dir/${name}`
```
Keep in mind, that `PowerShell` or `pwsh` requires a corresponding quote implementation. Define it [via helpers](./setup#bash) or manually:
```js
import { quotePowerShell } from 'zx'
$.quote = quotePowerShell
```
## Array of arguments
Zx can also accept an array of arguments within `${...}`. Each array item will be quoted separately and then joined by a
space.
```js
const flags = [
'--oneline',
'--decorate',
'--color',
]
await $`git log ${flags}`
```
## Glob patterns
Because Zx escapes everything inside `${...}`, you can't use glob syntax directly. Instead, Zx provides
a [`glob`](api.md#glob) function.
The following example won't work:
```js
const files = './**/*.md' // [!code error] // Incorrect
await $`ls ${files}`
```
The correct approach:
```js
const files = await glob('./**/*.md')
await $`ls ${files}`
```
## Home dir `~`
Zx won't expand the home directory symbol `~` if it's within `${...}`. Use `os.homedir()` for that purpose.
```js
const dir = `~/Downloads` // [!code error] // Incorrect
await $`ls ${dir}`
```
```js
await $`ls ${os.homedir()}/Downloads` // Correct
```
```js
await $`ls ~/Downloads` // Correct, ~ is outside of ${...}
```
## Assembling commands
If you're trying to dynamically assemble commands in Zx, you might run into limitations. For instance, the following
approach won't work:
```js
const cmd = 'rm'
if (force) cmd += ' -f'
if (recursive) cmd += ' -r'
cmd += ' ' + file
await $`${cmd}` // [!code error] // Incorrect
```
Zx will escape the entire string, making the command invalid. Instead, assemble an array of arguments and pass it to Zx
like this:
```js
const args = []
if (force) args.push('-f')
if (recursive) args.push('-r')
args.push(file)
await $`rm ${args}` // [!code hl]
```
================================================
FILE: docs/setup.md
================================================
# Setup
## Requirements
* Linux, macOS, or Windows
* JavaScript Runtime:
* Node.js >= 12.17.0
* Bun >= 1.0.0
* Deno 1.x, 2.x
* GraalVM Node.js
* Some kind of bash or PowerShell
## Install
::: code-group
```bash [npm]
npm install zx # add -g to install globally
```
```bash [npx]
npx zx script.js # run script without installing the zx package
npx zx@8.6.0 script.js # pin to a specific zx version
```
```bash [yarn]
yarn add zx
```
```bash [pnpm]
pnpm add zx
```
```bash [bun]
bun install zx
```
```bash [deno]
deno install -A npm:zx
# zx requires additional permissions: --allow-read --allow-sys --allow-env --allow-run
```
```bash [jsr]
npx jsr add @webpod/zx
deno add jsr:@webpod/zx
# https://jsr.io/docs/using-packages
```
```bash [docker]
docker pull ghcr.io/google/zx:8.5.0
docker run -t ghcr.io/google/zx:8.5.0 -e="await \$({verbose: true})\`echo foo\`"
docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js
```
```bash [brew]
brew install zx
```
:::
### Channels
zx is distributed in several versions, each with its own set of features.
| Channel | Description | Install |
|----------|----------------------------------------------------------------------------------------------|----------------------|
| `latest` | Mainline releases with the latest features and improvements. | `npm i zx` |
| `lite` | [A minimalistic version of zx](./lite), suitable for lightweight scripts. | `npm i zx@lite` |
| `dev` | Development snapshots with the latest changes, may be unstable. | `npm i zx@dev` |
| `legacy` | Legacy supporting versions for compatibility with older scripts, no new features, only bugfixes | `npm i zx@` |
Detailed comparison: [versions](./versions).
Please check the download sources carefully. Official links:
* [npmjs](https://www.npmjs.com/package/zx)
* [GH npm](https://github.com/google/zx/pkgs/npm/zx)
* [GH repo](https://github.com/google/zx)
* [GH docker](https://github.com/google/zx/pkgs/container/zx)
* [JSR](https://jsr.io/@webpod/zx)
* [Homebrew](https://github.com/Homebrew/homebrew-core/blob/master/Formula/z/zx.rb)
### Github
To fetch zx directly from the GitHub:
```bash
# Install via git
npm i google/zx
npm i git@github.com:google/zx.git
# Fetch from the GH pkg registry
npm i --registry=https://npm.pkg.github.com @google/zx
```
### Docker
If you'd prefer to run scripts in a container, you can pull the zx image from the [ghcr.io](https://ghcr.io).
[node:24-alpine](https://hub.docker.com/_/node) is used as [a base](https://github.com/google/zx/blob/main/dcr/Dockerfile).
```shell
docker pull ghcr.io/google/zx:8.5.0
docker run -t ghcr.io/google/zx:8.5.0 -e="await \$({verbose: true})\`echo foo\`"
docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js
```
## Bash
zx mostly relies on bash, so make sure it's available in your environment. If you're on Windows, consider using [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install) or [Git Bash](https://git-scm.com/downloads).
By default, zx looks for bash binary, but you can switch to PowerShell by invoking `usePowerShell()` or `usePwsh()`.
```js
import { useBash, usePowerShell, usePwsh } from 'zx'
usePowerShell() // Use PowerShell.exe
usePwsh() // Rely on pwsh binary (PowerShell v7+)
useBash() // Switch back to bash
```
## Package
### Hybrid
zx is distributed as a [hybrid package](https://2ality.com/2019/10/hybrid-npm-packages.html): it provides both CJS and ESM entry points.
```js
import { $ } from 'zx'
const { $ } = require('zx')
```
It also contains built-in TypeScript libdefs. But `@types/fs-extra` and `@types/node` are required to be installed on user's side.
```bash
npm i -D @types/fs-extra @types/node
```
```ts
import { type Options } from 'zx'
const opts: Options = {
quiet: true,
timeout: '5s'
}
```
### Bundled
We use [esbuild](https://dev.to/antongolub/how-and-why-do-we-bundle-zx-1ca6) to produce a static build that allows us to solve several issues at once:
* Reduce the pkg size and install time.
* Make npx (yarn dlx / bunx) invocations reproducible.
* Provide support for a wide range of Node.js versions: from [12 to 25](https://github.com/google/zx/blob/61d03329349770d90fda3c9e26f7ef09f869a096/.github/workflows/test.yml#L195).
* Make auditing easier: complete code is in one place.
### Composite
zx exports several entry points adapted for different use cases:
* `zx` – the main entry point, provides all the features.
* `zx/global` – to populate the global scope with zx functions.
* `zx/cli` – to run zx scripts from the command line.
* `zx/core` – to use zx template spawner as part of 3rd party libraries with alternating set of utilities.
### Typed
The library is written in TypeScript 5 and provides comprehensive type definitions for TS users.
* Libdefs are bundled via [dts-bundle-generator](https://github.com/timocov/dts-bundle-generator).
* Compatible with TS 4.0 and later.
* Requires `@types/node` and `@types/fs-extra` to be installed.
================================================
FILE: docs/shell.md
================================================
# Shell
[Bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) is a fundamental part of the Unix ecosystem, and it is widely used for scripting and automation tasks. It provides a powerful set of built-in utils, operators, process controllers.
Bash gives an efficient way to fine-tune the behavior: cmd aliases, context presets, custom functions, env injections, and more.
zx is not trying to replace bash, but to enhance it with JavaScript's capabilities:
* Parallel execution
* Data transformations
* Exception handling
* Conditional logic and loops
```js
#!/usr/bin/env zx
import { $ } from 'zx'
$.nothrow = true
const repos = ['zx', 'webpod']
const clones = repos
.map(n => $`git clone https://github.com/google/${n} ${n}-clone`)
const results = await Promise.all(clones)
const errors = results.filter(o => !o.ok).map(o => o.stderr.trim())
console.log('errors', errors.join('\n'))
for (p of clones) {
await p.pipe`cat > ${p.pid}.txt`
}
```
## Bash and Pwsh
There're many shell implementations. zx brings a few setup helpers:
* [`useBash`](./api#usebash) switches to bash
* [`usePowerShell`](./api#usepowershell) — PowerShell
* [`usePwsh`](./api#usepwsh) — pwsh (PowerShell v7+)
You can also set the shell directly via [JS API](./setup#bash), [CLI flags](./cli#shell) or [envars](./cli#environment-variables):
```js
$.shell = '/bin/zsh'
```
```bash
zx --shell /bin/zsh script.js
```
```bash
ZX_SHELL=/bin/zsh zx script.js
```
## zx = bash + js
No compromise, take the best of both.
================================================
FILE: docs/typescript.md
================================================
# TypeScript
zx is written in TypeScript and provides the corresponding libdefs out of the box. Types are TS 4+ compatible. Write code in any suitable format `.ts`, `.mts`, `.cts` or add [a custom loader](./cli#non-standard-extension).
```ts
// script.ts
import { $ } from 'zx'
const list = await $`ls -la`
```
Some runtimes like [Bun](https://bun.sh/) or [Deno](https://deno.com/) have built-in TS support. Node.js requires additional setup. Configure your project according to the [ES modules contract](https://nodejs.org/api/packages.html#packages_type):
- Set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_type)
in **package.json**
- Set [`"module": "ESNext"`](https://www.typescriptlang.org/tsconfig/#module)
in **tsconfig.json**.
Using TypeScript compiler is the most straightforward way, but native TS support from runtimes is gradually increasing.
::: code-group
```bash [node]
# Since Node.js v22.6.0
node --experimental-strip-types script.js
```
```bash [npx]
# Since Node.js v22.6.0
NODE_OPTIONS="--experimental-strip-types" zx script.js
```
```bash [tsc]
npm install typescript
tsc script.ts
node script.js
```
```bash [ts-node]
npm install ts-node
ts-node script.ts
# or via node loader
node --loader ts-node/esm script.ts
```
```bash [swc-node]
npm install swc-node
swc-node script.ts
```
```bash [tsx]
npm install tsx
tsx script.ts
# or
node --import=tsx script.ts
```
```bash [bun]
bun script.ts
```
```bash [deno]
deno run --allow-read --allow-sys --allow-env --allow-run script.ts
```
:::
================================================
FILE: docs/versions.md
================================================
# Versions
zx is distributed in several versions, each with its own set of features.
* `@latest` represents the stable full-featured version.
* `@lite` separates the zx core from the extensions.
* `@dev` brings experimental snapshots and RCs.
| Feature | latest | lite |
|-------------------|--------|------|
| **zx/globals** | ✔️ | ️ |
| **zx/cli** | ✔️ | |
| `$` | ✔️ | ✔️ |
| `ProcessPromise` | ✔️ | ✔️ |
| `ProcessOutput` | ✔️ | ✔️ |
| `argv` | ✔️ | ️ |
| `cd` | ✔️ | ✔️ |
| `chalk` | ✔️ | ✔️ |
| `defaults` | ✔️ | ✔️ |
| `dotenv` | ✔️ | ️ |
| `echo` | ✔️ | ️ |
| `expBackoff` | ✔️ | ️ |
| `fetch` | ✔️ | ️ |
| `fs` | ✔️ | ️ |
| `glob` | ✔️ | ️ |
| `kill` | ✔️ | ✔️ |
| `log` | ✔️ | ✔️ |
| `minimist` | ✔️ | ️ |
| `nothrow` | ✔️ | ️ |
| `os` | ✔️ | ✔️ |
| `parseArgv` | ✔️ | ️ |
| `path` | ✔️ | ✔️ |
| `ps` | ✔️ | ✔️ |
| `question` | ✔️ | ️ |
| `quiet` | ✔️ | ️ |
| `quote` | ✔️ | ✔️ |
| `quotePowerShell` | ✔️ | ✔️ |
| `resolveDefaults` | ✔️ | ✔️ |
| `retry` | ✔️ | ️ |
| `sleep` | ✔️ | ️ |
| `spinner` | ✔️ | ️ |
| `syncProcessCwd` | ✔️ | ✔️ |
| `tempdir` | ✔️ | |
| `tempfile` | ✔️ | |
| `updateArgv` | ✔️ | |
| `useBash` | ✔️ | ✔️ |
| `usePowerShell` | ✔️ | ✔️ |
| `usePwsh` | ✔️ | ✔️ |
| `version` | ✔️ | ️ |
| `which` | ✔️ | ✔️ |
| `within` | ✔️ | ✔️ |
| `YAML` | ✔️ | ️ |
| `MAML` | ✔️ | ️ |
================================================
FILE: examples/background-process.mjs
================================================
#!/usr/bin/env zx
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const serve = $`npx serve`
for await (const chunk of serve.stdout) {
if (chunk.includes('Accepting connections')) break
}
await $`curl http://localhost:3000`
serve.kill('SIGINT')
================================================
FILE: examples/backup-github.mjs
================================================
#!/usr/bin/env zx
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const username = await question('What is your GitHub username? ')
const token = await question('Do you have GitHub token in env? ', {
choices: Object.keys(process.env),
})
let headers = {}
if (process.env[token]) {
headers = {
Authorization: `token ${process.env[token]}`,
}
}
let res = await fetch(
`https://api.github.com/users/${username}/repos?per_page=1000`,
{ headers }
)
const data = await res.json()
const urls = data.map((x) =>
x.git_url.replace('git://github.com/', 'git@github.com:')
)
await $`mkdir -p backups`
cd('./backups')
for (const url of urls) {
await $`git clone ${url}`
}
================================================
FILE: examples/fetch-weather.mjs
================================================
#!/usr/bin/env zx
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
async function main() {
const argv = minimist(process.argv.slice(2), {
boolean: ['help'],
alias: { h: 'help' },
})
if (argv.help) {
echo(`
${chalk.bold('Usage:')} zx fetch-weather.mjs [city name]
Fetches weather data using wttr.in with a neat two-column colored table format.
${chalk.bold('Examples:')}
zx fetch-weather.mjs London
./fetch-weather.mjs "New York"
`)
process.exit(0)
}
const args = argv._.slice(__filename === process.argv[1] ? 0 : 1)
const city = args.join(' ')
if (!city) throw 'No city provided. Use -h for help.'
const svc_url = 'https://wttr.in'
const data = await spinner(
`📡 Fetching weather for "${city}" from ${svc_url}...`,
async () => {
try {
const res = await fetch(
`${svc_url}/${encodeURIComponent(city)}?format=j1`,
{
signal: AbortSignal.timeout(5000),
}
)
if (!res.ok) throw `API error: ${res.status} ${res.statusText}`
return res.json()
} catch (err) {
if (err.name === 'AbortError') {
throw 'Request timed out after 5 seconds.'
}
throw err
}
}
)
const area = data.nearest_area[0]
const current = data.current_condition[0]
if (!area || !current) {
throw '❌ Missing weather data in API response.'
}
const location = area.areaName[0].value
const condition = current.weatherDesc[0].value
const temperature = current.temp_C
const humidity = current.humidity
echo(chalk.yellow(`🌤️ Weather in ${location}: ${condition}`))
echo(chalk.red(`🌡️ Temperature: ${temperature}°C`))
echo(chalk.blue(`💧 Humidity: ${humidity}%`))
}
await main().then(
() => process.exit(0),
(err) => {
const msg = typeof err === 'string' ? err : err.message
echo(chalk.red(`❌ ${msg}`))
process.exit(1)
}
)
// Here's how to add this script to your shell as a bash alias. This assumes you have zx installed globally.
// 1. Save this script as `fetch-weather.mjs`.
// 2. Add the following line to your .bashrc file, replacing the path with your own:
// alias weather='zx /full/path/to/fetch-weather.mjs'
// 3. Then reload your shell using the following command:
// source ~/.bashrc
// Now you can use the `weather` command to fetch weather data for any city.
// Example usage: `weather London`
================================================
FILE: examples/hello.mjs
================================================
#!/usr/bin/env zx
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
await $({ verbose: true })`echo "Hello!"`
================================================
FILE: examples/interactive.mjs
================================================
#!/usr/bin/env zx
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const p = $`npm init`.stdio('pipe')
for await (const chunk of p.stdout) {
if (chunk.includes('package name:')) p.stdin.write('test\n')
if (chunk.includes('version:')) p.stdin.write('1.0.0\n')
if (chunk.includes('description:')) p.stdin.write('My test package\n')
if (chunk.includes('entry point:')) p.stdin.write('index.mjs\n')
if (chunk.includes('test command:')) p.stdin.write('test.mjs\n')
if (chunk.includes('git repository:')) p.stdin.write('my-org/repo\n')
if (chunk.includes('keywords:')) p.stdin.write('foo, bar\n')
if (chunk.includes('author:')) p.stdin.write('Anton Medvedev\n')
if (chunk.includes('license:')) p.stdin.write('MIT\n')
if (chunk.includes('Is this OK?')) p.stdin.write('yes\n')
}
================================================
FILE: examples/parallel.mjs
================================================
#!/usr/bin/env zx
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { spinner } from 'zx'
const tests = await glob('test/*.test.js')
await spinner('running tests', async () => {
try {
const res = await Promise.all(tests.map((file) => $`npx uvu . ${file}`))
res.forEach((r) => console.log(r.toString()))
console.log(chalk.bgGreen.black(' SUCCESS '))
} catch (e) {
console.log(e.toString())
process.exitCode = 1
}
})
================================================
FILE: lefthook.yml
================================================
pre-commit:
parallel: true
commands:
format:
glob: '*.{js,ts,md,yml,yaml}'
run: npm run fmt && git add {staged_files}
commit-msg:
commands:
lint-commit-msg:
run: npx commitlint --edit
pre-push:
parallel: true
commands:
license:
run: npm run test:license
size:
run: npm run test:size
circular:
run: npm run test:circular
================================================
FILE: man/zx.1
================================================
.\" Manpage for zx.
.TH man 8 "06 Jul 2024" "8.x" "zx man page"
.SH NAME
zx \- the zx CLI
.SH DESCRIPTION
A tool for writing better scripts.
.SH SYNOPSIS
.SS zx\fR [\fIOPTIONS\fR] \fIURI\fR
.SH OPTIONS
.SS --cwd
set current directory
.SS --quiet
suppress any outputs
.SS --verbose
enables verbose mode
.SS --shell=
set the shell to use
.SS --prefix=
prefix all commands
.SS --postfix=
postfix all commands
.SS --prefer-local, -l
prefer locally installed packages and binaries
.SS --eval=, -e
evaluate script
.SS --ext=<.mjs>
script extension
.SS --install, -i
install dependencies
.SS --registry=
npm registry, defaults to https://registry.npmjs.org/
.SS --repl
start repl
.SS --env=
path to env file
.SS --version, -v
print current zx version
.SS --help, -h
print command help and options
.SH EXAMPLES
.TP
.I zx --verbose script.js
.TP
.I zx https://example.com/script.js
.TP
.I zx -e '$`ls -l`'
.SH BUGS
https://github.com/google/zx/issues.
.SH AUTHOR
Anton Medvedev (https://medv.io/)
================================================
FILE: package.json
================================================
{
"name": "zx",
"version": "8.9.0",
"description": "A tool for writing better scripts",
"type": "module",
"main": "./build/index.cjs",
"types": "./build/index.d.ts",
"typesVersions": {
"*": {
".": [
"./build/index.d.ts"
],
"globals": [
"./build/globals.d.ts"
],
"cli": [
"./build/cli.d.ts"
],
"core": [
"./build/core.d.ts"
]
}
},
"exports": {
".": {
"types": "./build/index.d.ts",
"import": "./build/index.js",
"require": "./build/index.cjs",
"default": "./build/index.js"
},
"./globals": {
"types": "./build/globals.d.ts",
"import": "./build/globals.js",
"require": "./build/globals.cjs",
"default": "./build/globals.js"
},
"./cli": {
"types": "./build/cli.d.ts",
"import": "./build/cli.js",
"require": "./build/cli.cjs",
"default": "./build/cli.js"
},
"./core": {
"types": "./build/core.d.ts",
"import": "./build/core.js",
"require": "./build/core.cjs",
"default": "./build/core.js"
},
"./package.json": "./package.json"
},
"bin": {
"zx": "build/cli.js"
},
"man": "./man/zx.1",
"files": [
"build/3rd-party-licenses",
"build/cli.js",
"build/core.js",
"build/deno.js",
"build/globals.js",
"build/index.js",
"build/*.cjs",
"build/*.d.ts",
"man"
],
"engines": {
"node": ">= 12.17.0"
},
"scripts": {
"fmt": "prettier --write .",
"fmt:check": "prettier --check .",
"prebuild": "rm -rf build",
"build": "npm run build:versions && npm run build:js && npm run build:dts && npm run build:tests",
"build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry='src/{cli,core,deps,globals,index,internals,util,vendor*}.ts' && npm run build:vendor",
"build:vendor": "node scripts/build-js.mjs --format=cjs --entry=src/vendor-*.ts --bundle=all --external='./internals.ts'",
"build:versions": "node scripts/build-versions.mjs",
"build:tests": "node scripts/build-tests.mjs",
"build:dts": "tsc --project tsconfig.json && node scripts/build-dts.mjs",
"build:dcr": "docker build -f ./dcr/Dockerfile . -t zx",
"build:jsr": "node scripts/build-jsr.mjs",
"build:lite": "node scripts/build-pkgjson-lite.mjs",
"build:pkgjson": "node scripts/build-pkgjson-main.mjs",
"build:manifest": "npm run build:pkgjson && npm run build:lite && npm run build:jsr",
"postbuild": "node scripts/build-clean.mjs && npm run build:manifest",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"pretest": "npm run build",
"test": "npm run test:size && npm run fmt:check && npm run test:unit && npm run test:types && npm run test:license",
"test:npm": "node ./test/it/build-npm.test.js",
"test:jsr": "node ./test/it/build-jsr.test.js",
"test:dcr": "node ./test/it/build-dcr.test.js",
"test:unit": "node --experimental-transform-types ./test/all.test.js",
"test:coverage": "c8 -c .nycrc --check-coverage npm run test:unit",
"test:circular": "madge --circular src/*",
"test:types": "tsd",
"test:license": "node ./test/extra.test.js",
"test:audit": "npm audit --package-lock",
"test:size": "size-limit",
"test:smoke:strip-types": "node --experimental-strip-types test/smoke/ts.test.ts",
"test:smoke:tsx": "tsx test/smoke/ts.test.ts",
"test:smoke:tsc": "cd test/smoke && mkdir -p node_modules && ln -s ../../../ ./node_modules/zx; ../../node_modules/typescript/bin/tsc -v && ../../node_modules/typescript/bin/tsc --project tsconfig.test.json && node ./temp/ts.test.js",
"test:smoke:ts-node": "cd test/smoke && node --loader ts-node/esm ts.test.ts",
"test:smoke:bun": "bun test ./test/smoke/bun.test.js && bun ./test/smoke/node.test.mjs",
"test:smoke:win32": "node ./test/smoke/win32.test.js",
"test:smoke:cjs": "node ./test/smoke/node.test.cjs",
"test:smoke:mjs": "node ./test/smoke/node.test.mjs",
"test:smoke:deno": "deno test ./test/smoke/deno.test.js --allow-read --allow-sys --allow-env --allow-run",
"test:workflow": "zizmor .github/workflows -v -p --min-severity=medium"
},
"devDependencies": {
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@size-limit/file": "12.0.0",
"@types/fs-extra": "11.0.4",
"@types/minimist": "1.2.5",
"@types/node": "25.3.2",
"@types/which": "3.0.4",
"@webpod/ingrid": "1.1.1",
"@webpod/ps": "1.0.0",
"c8": "11.0.0",
"chalk": "5.6.2",
"create-require": "1.1.1",
"cronometro": "6.0.3",
"depseek": "0.4.3",
"dts-bundle-generator": "9.5.1",
"envapi": "0.2.3",
"esbuild": "0.27.3",
"esbuild-node-externals": "1.20.1",
"esbuild-plugin-entry-chunks": "0.1.17",
"esbuild-plugin-extract-helpers": "0.0.6",
"esbuild-plugin-hybrid-export": "0.3.1",
"esbuild-plugin-resolve": "2.0.0",
"esbuild-plugin-transform-hook": "0.2.0",
"esbuild-plugin-utils": "0.1.0",
"fs-extra": "11.3.3",
"get-port": "7.1.0",
"globby": "16.1.1",
"jsr": "0.14.3",
"lefthook": "2.1.1",
"madge": "8.0.0",
"maml.js": "^0.0.3",
"minimist": "1.2.8",
"node-fetch-native": "1.6.7",
"prettier": "3.8.1",
"size-limit": "12.0.0",
"ts-node": "10.9.2",
"tsd": "0.33.0",
"tsx": "4.21.0",
"typescript": "5.9.3",
"vitepress": "1.6.4",
"which": "6.0.1",
"yaml": "2.8.2",
"zurk": "0.11.10"
},
"overrides": {
"globby": {
"fast-glob": "3.3.3"
},
"tsx": {
"esbuild": "$esbuild"
},
"vite": {
"esbuild": "$esbuild"
},
"@webpod/ps": {
"zurk": "$zurk"
}
},
"publishConfig": {
"registry": "https://wombat-dressing-room.appspot.com"
},
"keywords": [
"bash",
"bin",
"binary",
"call",
"child",
"child_process",
"exec",
"execute",
"invoke",
"pipe",
"process",
"script",
"shell",
"spawn",
"zx"
],
"prettier": {
"semi": false,
"singleQuote": true,
"endOfLine": "lf",
"trailingComma": "es5"
},
"repository": {
"type": "git",
"url": "git+https://github.com/google/zx.git"
},
"homepage": "https://google.github.io/zx/",
"author": "Anton Medvedev ",
"license": "Apache-2.0",
"volta": {
"node": "24.13.1"
},
"tsd": {
"compilerOptions": {
"rootDir": "."
}
}
}
================================================
FILE: scripts/build-clean.mjs
================================================
#!/usr/bin/env node
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import fs from 'node:fs'
import glob from 'fast-glob'
const redundants = await glob(
['build/{repl,globals-jsr}.d.ts', 'build/{deps,internals,util,vendor*}.js'],
{
onlyFiles: true,
absolute: true,
}
)
for (const file of redundants) {
fs.unlinkSync(file)
}
console.log('postbuild removed', redundants)
================================================
FILE: scripts/build-dts.mjs
================================================
#!/usr/bin/env node
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import fs from 'node:fs/promises'
import { generateDtsBundle } from 'dts-bundle-generator'
import glob from 'fast-glob'
const output = {
inlineDeclareExternals: true,
inlineDeclareGlobals: true,
sortNodes: false,
exportReferencedTypes: false, //args['export-referenced-types'],
}
const entries = [
{
filePath: './src/vendor-extra.ts',
outFile: './build/vendor-extra.d.ts',
libraries: {
allowedTypesLibraries: ['node'], // args['external-types'],
inlinedLibraries: [
'@nodelib/fs.stat',
'@nodelib/fs.scandir',
'@nodelib/fs.walk',
'fast-glob',
'@types/jsonfile',
'node-fetch-native',
// 'chalk',
'globby',
'@types/minimist',
// '@types/which',
// 'zurk',
// '@webpod/ps',
'@webpod/ingrid',
'depseek',
'envapi',
'maml.js',
], // args['external-inlines'],
},
output,
},
{
filePath: './src/vendor-core.ts',
outFile: './build/vendor-core.d.ts',
libraries: {
allowedTypesLibraries: ['node'], // args['external-types'],
inlinedLibraries: [
'@types/which',
'@webpod/ps',
'@webpod/ingrid',
'chalk',
'zurk',
], // args['external-inlines'],
},
output,
},
]
const compilationOptions = {
preferredConfigPath: './tsconfig.json', // args.project,
followSymlinks: true,
}
const results = generateDtsBundle(entries, compilationOptions)
// generateDtsBundle cannot handle the circular refs on types inlining, so we need to help it manually:
/*
build/vendor.d.ts(163,7): error TS2456: Type alias 'Options' circularly references itself.
build/vendor.d.ts(164,7): error TS2456: Type alias 'Entry' circularly references itself.
build/vendor.d.ts(165,7): error TS2456: Type alias 'Task' circularly references itself.
build/vendor.d.ts(166,7): error TS2456: Type alias 'Pattern' circularly references itself.
build/vendor.d.ts(167,7): error TS2456: Type alias 'FileSystemAdapter' circularly references itself.
build/vendor.d.ts(197,48): error TS2694: Namespace 'FastGlob' has no exported member 'FastGlobOptions
*/
.map((r) =>
r
.replace('type Options = Options;', 'export {Options};')
.replace('type Task = Task;', 'export {Task};')
.replace('type Pattern = Pattern;', 'export {Pattern};')
.replace('FastGlob.FastGlobOptions', 'FastGlob.Options')
.replace('type Entry =', 'export type Entry =')
)
for (const i in results) {
const entry = entries[i]
const result = results[i]
await fs.writeFile(entry.outFile, result, 'utf8')
}
// Properly formats triple-slash directives
const pkgEntries = ['core', 'index', 'vendor']
const prefix = `///
///
`
for (const dts of await glob(['build/**/*.d.ts', '!build/vendor-*.d.ts'])) {
const contents =
(pkgEntries.some((e) => dts.includes(e)) ? prefix : '') +
(await fs.readFile(dts, 'utf8'))
.replaceAll(".ts';", ".js';")
.split('\n')
.filter((line) => !line.startsWith('/// path.relative(cwd, path.resolve(cwd, p)))
const _bundle = bundle && bundle !== 'none'
const _external = ['zx/globals', ...(_bundle ? external.split(',') : [])] // https://github.com/evanw/esbuild/issues/1466
const plugins = [
esbuildResolvePlugin({
yaml: path.resolve(__dirname, '../node_modules/yaml/browser'),
}),
]
const thirdPartyModules = new Set()
if (_bundle && entryPoints.length > 1) {
plugins.push(entryChunksPlugin())
}
if (bundle === 'src') {
// https://github.com/evanw/esbuild/issues/619
// https://github.com/pradel/esbuild-node-externals/pull/52
plugins.push(nodeExternalsPlugin())
}
if (hybrid) {
plugins.push(
hybridExportPlugin({
loader: 'reexport',
to: 'build',
toExt: '.js',
})
)
}
plugins.push(
{
name: 'get-3rd-party-modules',
setup: (build) => {
build.onResolve({ filter: /./, namespace: 'file' }, async (args) => {
thirdPartyModules.add(args.resolveDir)
})
},
},
transformHookPlugin({
hooks: [
{
on: 'end',
if: !hybrid,
pattern: /\.js$/,
transform(contents, file) {
const { name } = path.parse(file)
const _contents = contents
.toString()
.replace(
'} = __module__',
`} = globalThis.Deno ? globalThis.require("./${name}.cjs") : __module__`
)
return injectCode(_contents, `import "./deno.js"`)
},
},
{
on: 'end',
if: !hybrid,
pattern: /cli\.js$/,
transform(contents) {
return `${contents}autorun(import.meta)
`
},
},
{
on: 'end',
pattern: entryPointsToRegexp(entryPoints),
transform(contents) {
const extras = [
// https://github.com/evanw/esbuild/issues/1633
contents.includes('import_meta')
? './scripts/import-meta-url.polyfill.js'
: '',
//https://github.com/evanw/esbuild/issues/1921
// p.includes('vendor') ? './scripts/require.polyfill.js' : '',
].filter(Boolean)
return injectFile(contents, ...extras)
},
},
{
on: 'end',
pattern: entryPointsToRegexp(entryPoints),
transform(contents) {
return contents
.toString()
.replaceAll('import.meta.url', 'import_meta_url')
.replaceAll('import_meta.url', 'import_meta_url')
.replaceAll('"node:', '"')
.replaceAll(
'require("stream/promises")',
'require("stream").promises'
)
.replaceAll('require("fs/promises")', 'require("fs").promises')
.replaceAll('}).prototype', '}).prototype || {}')
.replace(/DISABLE_NODE_FETCH_NATIVE_WARN/, ($0) => `${$0} || true`)
.replace(
/\/\/ Annotate the CommonJS export names for ESM import in node:/,
($0) => `/* c8 ignore next 100 */\n${$0}`
)
.replace(
'yield import("zx/globals")',
'yield require("./globals.cjs")'
)
.replace('require("./internals.ts")', 'require("./internals.cjs")')
},
},
],
}),
extractHelpersPlugin({
cwd: 'build',
include: /\.cjs/,
}),
{
name: 'deno',
setup(build) {
build.onEnd(() => {
fs.copyFileSync('./scripts/deno.polyfill.js', './build/deno.js')
fs.writeFileSync(
'./build/3rd-party-licenses',
digestLicenses(thirdPartyModules)
)
})
},
}
)
// prettier-ignore
function digestLicenses(dirs) {
const digest = [...[...dirs]
.reduce((m, d) => {
const chunks = d.split('/')
const i = chunks.lastIndexOf('node_modules')
const name = chunks[i + 1]
const shift = i + 1 + (name.startsWith('@') ? 2 : 1)
const root = chunks.slice(0, shift).join('/')
m.add(root)
return m
}, new Set())]
.map(d => {
const extractName = (entry) => entry?.name ? `${entry.name} <${entry.email}>` : entry
const pkg = path.join(d, 'package.json')
const pkgJson = JSON.parse(fs.readFileSync(pkg, 'utf-8'))
const author = extractName(pkgJson.author)
const contributors = (pkgJson.contributors || pkgJson.maintainers || []).map(extractName).join(', ')
const by = author || contributors || ''
const repository = pkgJson.repository?.url || pkgJson.repository || ''
const license = pkgJson.license || ''
if (pkgJson.name === 'zx') return
return `${pkgJson.name}@${pkgJson.version}
${by}
${repository}
${license}`
})
.filter(Boolean)
.sort()
.join('\n\n')
return `THIRD PARTY LICENSES
${digest}
`
}
function entryPointsToRegexp(entryPoints) {
return new RegExp(
'(' +
entryPoints.map((e) => escapeRegExp(path.parse(e).name)).join('|') +
')\\.cjs$'
)
}
function escapeRegExp(str) {
return str.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
}
const esmConfig = {
absWorkingDir: cwd,
entryPoints,
outdir: './build',
bundle: _bundle,
external: _external,
minify,
sourcemap,
sourcesContent: false,
platform: 'node',
target: 'esnext',
format: 'esm',
outExtension: {
'.js': '.mjs',
},
plugins,
legalComments: license,
tsconfig: './tsconfig.json',
}
const cjsConfig = {
...esmConfig,
outdir: './build',
target: 'es6',
format: 'cjs',
outExtension: {
'.js': '.cjs',
},
}
for (const format of formats) {
const config = format === 'cjs' ? cjsConfig : esmConfig
console.log('esbuild config:', config)
await esbuild.build(config).catch(() => process.exit(1))
}
process.exit(0)
================================================
FILE: scripts/build-jsr.mjs
================================================
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import fs from 'node:fs'
import path from 'node:path'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const root = path.resolve(__dirname, '..')
const pkgJson = JSON.parse(
fs.readFileSync(path.resolve(root, 'package.json'), 'utf-8')
)
const deps = pkgJson.devDependencies
const jsrDeps = {
yaml: 'jsr:@eemeli/yaml',
zurk: 'jsr:@webpod/zurk',
}
const prodDeps = new Set([
'@types/fs-extra',
'@types/minimist',
'@types/node',
'@types/which',
'@webpod/ingrid',
'@webpod/ps',
'chalk',
'create-require',
'depseek',
'envapi',
'fs-extra',
'globby',
'minimist',
'node-fetch-native',
'which',
'yaml',
'zurk',
])
fs.writeFileSync(
path.resolve(root, 'jsr.json'),
JSON.stringify(
{
name: '@webpod/zx',
version: pkgJson.version,
license: pkgJson.license,
exports: {
'.': './src/index.ts',
'./core': './src/core.ts',
'./cli': './src/cli.ts',
'./globals': './src/globals-jsr.ts',
},
publish: {
include: ['src', 'README.md', 'LICENSE'],
exclude: ['src/globals.ts'],
},
nodeModulesDir: 'auto',
imports: Object.entries(deps).reduce(
(m, [k, v]) => {
if (prodDeps.has(k)) {
const name = jsrDeps[k] || `npm:${k}`
m[k] = `${name}@${v}`
}
return m
},
{
'zurk/spawn': `jsr:@webpod/zurk@${deps.zurk}`,
'zx/globals': './src/globals-jsr.ts',
}
),
},
null,
2
)
)
console.log('jsr.json prepared for JSR')
================================================
FILE: scripts/build-pkgjson-lite.mjs
================================================
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Prepares a lite (core) version of zx to publish
import fs from 'node:fs'
import path from 'node:path'
import { depseekSync } from 'depseek'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const root = path.resolve(__dirname, '..')
const source = 'package.json'
const dest = 'package-lite.json'
const _pkgJson = JSON.parse(fs.readFileSync(path.join(root, source), 'utf-8'))
const files = new Set()
const entries = new Set(['./core.js', './3rd-party-licenses'])
for (const entry of entries) {
if (!fs.existsSync(path.join(root, 'build', entry))) continue
files.add(entry)
const contents = fs.readFileSync(path.join(root, 'build', entry), 'utf-8')
const deps = depseekSync(contents)
for (const { value: file } of deps) {
if (file.startsWith('.')) {
entries.add(file)
entries.add(file.replace(/\.c?js$/, '.d.ts'))
}
}
}
const whitelist = new Set([
'name',
'version',
'description',
'type',
'main',
'types',
'typesVersions',
'exports',
'files',
'engines',
'optionalDependencies',
'publishConfig',
'keywords',
'repository',
'homepage',
'author',
'license',
])
const __pkgJson = Object.fromEntries(
Object.entries(_pkgJson).filter(([k]) => whitelist.has(k))
)
const pkgJson = {
...__pkgJson,
version: _pkgJson.version + '-lite',
exports: {
'.': {
types: './build/core.d.ts',
import: './build/core.js',
require: './build/core.cjs',
default: './build/core.js',
},
'./package.json': './package.json',
},
main: './build/core.cjs',
types: './build/core.d.ts',
typesVersions: {
'*': {
'.': ['./build/core.d.ts'],
},
},
files: [...files].map((f) => path.join('build', f)).sort(),
}
fs.writeFileSync(path.resolve(root, dest), JSON.stringify(pkgJson, null, 2))
console.log(`${dest} prepared for npm`)
================================================
FILE: scripts/build-pkgjson-main.mjs
================================================
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Optimizes package.json for npm publishing
import fs from 'node:fs'
import path from 'node:path'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const root = path.resolve(__dirname, '..')
const source = 'package.json'
const dest = 'package-main.json'
const _pkgJson = JSON.parse(fs.readFileSync(path.join(root, source), 'utf-8'))
const whitelist = new Set([
'name',
'version',
'description',
'type',
'main',
'types',
'typesVersions',
'exports',
'bin',
'man',
'files',
'engines',
'optionalDependencies',
'publishConfig',
'keywords',
'repository',
'homepage',
'author',
'license',
])
const pkgJson = Object.fromEntries(
Object.entries(_pkgJson).filter(([k]) => whitelist.has(k))
)
fs.writeFileSync(path.resolve(root, dest), JSON.stringify(pkgJson, null, 2))
console.log(`${dest} prepared for npm`)
================================================
FILE: scripts/build-tests.mjs
================================================
#!/usr/bin/env node
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import fs from 'node:fs'
import path from 'node:path'
import * as core from '../build/core.js'
import * as cli from '../build/cli.js'
import * as index from '../build/index.js'
// prettier-ignore
const modules = [
['core', core],
['cli', cli],
['index', index],
]
const root = path.resolve(new URL(import.meta.url).pathname, '../..')
const filePath = path.resolve(root, `test/export.test.js`)
const copyright = fs.readFileSync(
path.resolve(root, 'test/fixtures/copyright.txt'),
'utf8'
)
let head = `${copyright.replace('YEAR', new Date().getFullYear())}
import assert from 'node:assert'
import { test, describe } from 'node:test'`
let body = '\n'
for (const [name, ref, apis = Object.keys(ref).sort()] of modules) {
head += `\nimport * as ${name} from '../build/${name}.cjs'`
body += `\n//prettier-ignore\ndescribe('${name}', () => {\n`
body += ` test('exports', () => {\n`
for (const r of apis) {
const api = ref[r]
body += ` assert.equal(typeof ${name}.${r}, '${typeof api}', '${name}.${r}')\n`
if (typeof api !== 'function' && typeof api !== 'object') continue
for (const k of Object.keys(api).sort()) {
const v = api[k]
body += ` assert.equal(typeof ${name}.${r}.${k}, '${typeof v}', '${name}.${r}.${k}')\n`
}
}
body += ' })\n'
body += '})\n'
}
const contents = head + body
fs.writeFileSync(filePath, contents)
================================================
FILE: scripts/build-versions.mjs
================================================
#!/usr/bin/env node
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import fs from 'fs-extra'
import path from 'node:path'
import minimist from 'minimist'
const root = path.resolve(new URL(import.meta.url).pathname, '../..')
const copyright = fs.readFileSync(
path.resolve(root, 'test/fixtures/copyright.txt'),
'utf8'
)
const license = copyright.replace('YEAR', new Date().getFullYear())
const deps = [
'chalk',
'depseek',
'dotenv',
'fetch',
'fs',
'glob',
'minimist',
'ps',
'which',
'yaml',
]
const namemap = {
dotenv: 'envapi',
fs: 'fs-extra',
fetch: 'node-fetch-native',
glob: 'globby',
ps: '@webpod/ps',
}
const versions = deps.reduce(
(m, name) => {
m[name] = fs.readJsonSync(
path.resolve(root, 'node_modules', namemap[name] || name, 'package.json')
).version
return m
},
{
zx: fs.readJsonSync(path.join(root, 'package.json')).version,
}
)
const argv = minimist(process.argv.slice(2), {
default: versions,
string: ['zx', ...deps],
})
delete argv._
const list = JSON.stringify(argv, null, 2)
.replaceAll(' "', ' ')
.replaceAll('": ', ': ')
.replaceAll('"', "'")
.replace(/\n}$/, ',\n}')
const versionsTs = `${license}
export const versions: Record = ${list}
`
const versionsCjs = `${license}
module.exports = { versions: ${list}
`
fs.writeFileSync(path.join(root, 'src/versions.ts'), versionsTs, 'utf8')
// fs.writeFileSync(path.join(root, 'build/versions.cjs'), versionsCjs, 'utf8')
================================================
FILE: scripts/deno.polyfill.js
================================================
import { createRequire } from 'node:module'
import * as process from 'node:process'
// prettier-ignore
if (globalThis.Deno) {
globalThis.require = createRequire(import.meta.url)
globalThis.__filename = new URL(import.meta.url).pathname
globalThis.__dirname = new URL('.', import.meta.url).pathname
globalThis.module = new Proxy({}, { set() { return true } })
const p = globalThis.process = globalThis.process || process
p.version || (p.version = 'v18.0.0')
p.version || (p.version = { node: '18.0.0' })
p.env || (p.env = globalThis.Deno.env.toObject())
p.argv || (p.argv = [globalThis.Deno.execPath(), globalThis.Deno.mainModule.replace('file://', ''), ...globalThis.Deno.args])
}
================================================
FILE: scripts/import-meta-url.polyfill.js
================================================
const import_meta_url =
typeof document === 'undefined'
? new (require('url').URL)('file:' + __filename).href
: (document.currentScript && document.currentScript.src) ||
new URL('main.js', document.baseURI).href
================================================
FILE: src/cli.ts
================================================
#!/usr/bin/env node
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import url from 'node:url'
import process from 'node:process'
import {
$,
ProcessOutput,
parseArgv,
updateArgv,
resolveDefaults,
chalk,
dotenv,
fetch,
fs,
path,
stdin,
VERSION,
Fail,
} from './index.ts'
import { installDeps, parseDeps } from './deps.ts'
import { startRepl } from './repl.ts'
import { randomId } from './util.ts'
import { transformMarkdown } from './md.ts'
import { createRequire, type minimist } from './vendor.ts'
export { transformMarkdown } from './md.ts'
const EXT = '.mjs'
const EXT_RE = /^\.[mc]?[jt]sx?$/
// prettier-ignore
export const argv: minimist.ParsedArgs = parseArgv(process.argv.slice(2), {
default: resolveDefaults({ ['prefer-local']: false } as any, 'ZX_', process.env, new Set(['env', 'install', 'registry'])),
// exclude 'prefer-local' to let minimist infer the type
string: ['shell', 'prefix', 'postfix', 'eval', 'cwd', 'ext', 'registry', 'env'],
boolean: ['version', 'help', 'quiet', 'verbose', 'install', 'repl', 'experimental'],
alias: { e: 'eval', i: 'install', v: 'version', h: 'help', l: 'prefer-local', 'env-file': 'env' },
stopEarly: true,
parseBoolean: true,
camelCase: true,
})
autorun(import.meta)
export function autorun(meta: ImportMeta): void {
if (meta && isMain(meta))
main().catch((err) => {
if (err instanceof ProcessOutput) {
console.error('Error:', err.message)
} else {
console.error(err)
}
process.exitCode = 1
})
}
export function printUsage() {
// language=txt
console.log(`
${chalk.bold('zx ' + VERSION)}
A tool for writing better scripts
${chalk.bold('Usage')}
zx [options]