diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6234abf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,220 @@ +name: CI + +on: + push: + branches: + - staging + - feature* + +jobs: + check-lint: + name: "Check / Lint" + runs-on: ubuntu-latest + container: + image: ghcr.io/matrixai/github-runner + steps: + - uses: actions/checkout@v4 + - name: Run linting + run: | + nix develop --command bash -c $' + npm run lint + npm run lint-shell + ' + + check-test: + name: "Check / Test" + runs-on: ubuntu-latest + container: + image: ghcr.io/matrixai/github-runner + steps: + - uses: actions/checkout@v4 + - name: Run tests + run: | + nix develop .#ci --command bash -c $' + npm test -- --ci --coverage + ' + - uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: tmp/coverage/cobertura-coverage.xml + + build-pull: + name: "Build / Pull Request" + runs-on: ubuntu-latest + needs: [check-lint, check-test] + if: github.ref == 'refs/heads/staging' + steps: + - uses: actions/checkout@v4 + - name: Create pull request + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + gh pr create \ + --head staging \ + --base master \ + --title "ci: merge staging to master" \ + --body "This is an automatic PR generated by the CI/CD pipeline. This will be automatically fast-forward merged if successful." \ + --assignee "@me" \ + --no-maintainer-edit || true + printf "Pipeline Attempt on $GITHUB_RUN_ID for $GITHUB_SHA\n\n$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + | gh pr comment staging \ + --body-file - \ + --repo "$GITHUB_REPOSITORY" + + build-dist: + name: "Build / Dist" + runs-on: ubuntu-latest + container: + image: ghcr.io/matrixai/github-runner + needs: [check-lint, check-test] + if: github.ref == 'refs/heads/staging' + steps: + - uses: actions/checkout@v4 + - run: | + nix develop .#ci --command bash -c $' + npm run build --ignore-scripts --verbose + ' + - uses: actions/upload-artifact@v4 + with: + name: dist + path: ./dist + + build-platforms: + name: "Build / Platforms" + runs-on: ${{ matrix.os }} + container: + image: ${{ matrix.platform == 'linux' && 'ghcr.io/matrixai/github-runner' || null }} + needs: build-dist + if: github.ref == 'refs/heads/staging' + strategy: + fail-fast: false + matrix: + include: + - platform: linux + os: ubuntu-latest + script: | + nix develop .#ci --command bash -c $' + npm test -- --ci --coverage + npm run bench + ' + - platform: windows + os: windows-latest + script: | + ./scripts/choco-install.ps1 + refreshenv + npm install --ignore-scripts + $env:Path = "$(npm root)\.bin;" + $env:Path + npm test -- --ci --coverage + npm run bench + - platform: macos + os: macos-latest + script: | + eval "$(brew shellenv)" + ./scripts/brew-install.sh + hash -r + npm install --ignore-scripts + export PATH="$(npm root)/.bin:$PATH" + npm test -- --ci --coverage + npm run bench + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - uses: actions/download-artifact@v4 + with: + name: dist + path: ./dist + - name: Build + run: ${{ matrix.script }} + - uses: actions/upload-artifact@v4 + with: + name: builds-${{ matrix.platform }} + path: ./builds + + build-prerelease: + name: "Build / Pre-release" + runs-on: ubuntu-latest + container: + image: ghcr.io/matrixai/github-runner + concurrency: + group: build-prerelease + cancel-in-progress: false + needs: [check-lint, check-test] + if: > + github.ref == 'refs/heads/staging' && + startsWith(github.ref, 'refs/tags/v') && + contains(github.ref, '-') + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: builds* + path: builds + merge-multiple: true + - name: Run pre-release + run: | + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ./.npmrc + echo 'Publishing library prerelease' + nix develop .#ci --command bash -c $' + npm publish --tag prerelease --access public + ' + rm -f ./.npmrc + + integration-merge: + name: "Integration / Merge" + runs-on: ubuntu-latest + concurrency: + group: integration-merge + cancel-in-progress: true + needs: [build-pull, build-platforms] + if: github.ref == 'refs/heads/staging' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN }} + - name: Merge into master + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} + GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} + GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} + GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }} + run: | + printf "Pipeline Succeeded on $GITHUB_RUN_ID for $GITHUB_SHA\n\n$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + | gh pr comment staging \ + --body-file - \ + --repo "$GITHUB_REPOSITORY" + git checkout master + git merge --ff-only "$GITHUB_SHA" + git push origin master + + release-distribution: + name: "Release / Distribution" + runs-on: ubuntu-latest + container: + image: ghcr.io/matrixai/github-runner + concurrency: + group: release-distribution + cancel-in-progress: false + needs: integration-merge + if: > + github.ref == 'refs/heads/staging' && + startsWith(github.ref, 'refs/tags/v') && + !contains(github.ref, '-') + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: builds* + path: builds + merge-multiple: true + - name: Run release + run: | + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ./.npmrc + echo 'Publishing library' + nix develop .#ci --command bash -c $' + npm publish --access public + ' + rm -f ./.npmrc diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml deleted file mode 100644 index a72f58b..0000000 --- a/.github/workflows/codesee-arch-diagram.yml +++ /dev/null @@ -1,23 +0,0 @@ -# This workflow was added by CodeSee. Learn more at https://codesee.io/ -# This is v2.0 of this workflow file -on: - push: - branches: - - master - pull_request_target: - types: [opened, synchronize, reopened] - -name: CodeSee - -permissions: read-all - -jobs: - codesee: - runs-on: ubuntu-latest - continue-on-error: true - name: Analyze the repo with CodeSee - steps: - - uses: Codesee-io/codesee-action@v2 - with: - codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} - codesee-url: https://app.codesee.io diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 399665c..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,308 +0,0 @@ -workflow: - rules: - # Disable merge request pipelines - - if: $CI_MERGE_REQUEST_ID - when: never - - when: always - -variables: - GIT_SUBMODULE_STRATEGY: recursive - GH_PROJECT_PATH: "MatrixAI/${CI_PROJECT_NAME}" - GH_PROJECT_URL: "https://${GITHUB_TOKEN}@github.com/${GH_PROJECT_PATH}.git" - # Cache .npm - npm_config_cache: "${CI_PROJECT_DIR}/tmp/npm" - # Prefer offline node module installation - npm_config_prefer_offline: "true" - # Homebrew cache only used by macos runner - HOMEBREW_CACHE: "${CI_PROJECT_DIR}/tmp/Homebrew" - -default: - interruptible: true - before_script: - # Replace this in windows runners that use powershell - # with `mkdir -Force "$CI_PROJECT_DIR/tmp"` - - mkdir -p "$CI_PROJECT_DIR/tmp" - -# Cached directories shared between jobs & pipelines per-branch per-runner -cache: - key: $CI_COMMIT_REF_SLUG - # Preserve cache even if job fails - when: 'always' - paths: - - ./tmp/npm/ - # Homebrew cache is only used by the macos runner - - ./tmp/Homebrew - # Chocolatey cache is only used by the windows runner - - ./tmp/chocolatey/ - # `jest` cache is configured in jest.config.js - - ./tmp/jest/ - -stages: - - check # Linting, unit tests - - build # Cross-platform library compilation, unit tests - - integration # Cross-platform application bundling, integration tests, and pre-release - - release # Cross-platform distribution and deployment - -image: registry.gitlab.com/matrixai/engineering/maintenance/gitlab-runner - -check:lint: - stage: check - needs: [] - script: - - > - nix-shell --arg ci true --run $' - npm run lint; - npm run lint-shell; - ' - rules: - # Runs on feature and staging commits and ignores version commits - - if: $CI_COMMIT_BRANCH =~ /^(?:feature.*|staging)$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Runs on tag pipeline where the tag is a prerelease or release version - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Manually run on commits other than master and ignore version commits - - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != 'master' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - when: manual - -check:test: - stage: check - needs: [] - script: - - > - nix-shell --arg ci true --run $' - npm test -- --ci --coverage; - ' - artifacts: - when: always - reports: - junit: - - ./tmp/junit/junit.xml - coverage_report: - coverage_format: cobertura - path: ./tmp/coverage/cobertura-coverage.xml - coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' - rules: - # Runs on feature commits and ignores version commits - - if: $CI_COMMIT_BRANCH =~ /^feature.*$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Manually run on commits other than master and staging and ignore version commits - - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH !~ /^(?:master|staging)$/ && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - when: manual - -build:merge: - stage: build - needs: [] - allow_failure: true - script: - # Required for `gh pr create` - - git remote add upstream "$GH_PROJECT_URL" - - > - nix-shell --arg ci true --run $' - gh pr create \ - --head staging \ - --base master \ - --title "ci: merge staging to master" \ - --body "This is an automatic PR generated by the pipeline CI/CD. This will be automatically fast-forward merged if successful." \ - --assignee "@me" \ - --no-maintainer-edit \ - --repo "$GH_PROJECT_PATH" || true; - printf "Pipeline Attempt on ${CI_PIPELINE_ID} for ${CI_COMMIT_SHA}\n\n${CI_PIPELINE_URL}" \ - | gh pr comment staging \ - --body-file - \ - --repo "$GH_PROJECT_PATH"; - ' - rules: - # Runs on staging commits and ignores version commits - - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Runs on tag pipeline where the tag is a prerelease or release version - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - -build:dist: - stage: build - needs: [] - script: - - > - nix-shell --arg ci true --run $' - npm run build --verbose; - ' - artifacts: - when: always - paths: - - ./dist - rules: - # Runs on staging commits and ignores version commits - - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Runs on tag pipeline where the tag is a prerelease or release version - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - -build:linux: - stage: build - needs: [] - script: - - > - nix-shell --arg ci true --run $' - npm test -- --ci --coverage; - npm run bench; - ' - artifacts: - when: always - reports: - junit: - - ./tmp/junit/junit.xml - coverage_report: - coverage_format: cobertura - path: ./tmp/coverage/cobertura-coverage.xml - metrics: ./benches/results/metrics.txt - coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' - rules: - # Runs on staging commits and ignores version commits - - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Runs on tag pipeline where the tag is a prerelease or release version - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - -build:windows: - stage: build - needs: [] - tags: - - windows - before_script: - - mkdir -Force "$CI_PROJECT_DIR/tmp" - - Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1 - script: - - .\scripts\choco-install.ps1 - - refreshenv - - npm install --ignore-scripts - - $env:Path = "$(npm root)\.bin;" + $env:Path - - npm test -- --ci --coverage - - npm run bench - artifacts: - when: always - reports: - junit: - - ./tmp/junit/junit.xml - coverage_report: - coverage_format: cobertura - path: ./tmp/coverage/cobertura-coverage.xml - metrics: ./benches/results/metrics.txt - coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' - rules: - # Runs on staging commits and ignores version commits - - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Runs on tag pipeline where the tag is a prerelease or release version - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - -build:macos: - stage: build - needs: [] - tags: - - saas-macos-medium-m1 - image: macos-12-xcode-14 - script: - - eval "$(brew shellenv)" - - ./scripts/brew-install.sh - - hash -r - - npm install --ignore-scripts - - export PATH="$(npm root)/.bin:$PATH" - - npm test -- --ci --coverage - - npm run bench - artifacts: - when: always - reports: - junit: - - ./tmp/junit/junit.xml - coverage_report: - coverage_format: cobertura - path: ./tmp/coverage/cobertura-coverage.xml - metrics: ./benches/results/metrics.txt - coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' - rules: - # Runs on staging commits and ignores version commits - - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Runs on tag pipeline where the tag is a prerelease or release version - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - -build:prerelease: - stage: build - needs: - - build:dist - - build:linux - - build:windows - - build:macos - # Don't interrupt publishing job - interruptible: false - script: - - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ./.npmrc - - echo 'Publishing library prerelease' - - > - nix-shell --arg ci true --run $' - npm publish --tag prerelease --access public; - ' - after_script: - - rm -f ./.npmrc - rules: - # Only runs on tag pipeline where the tag is a prerelease version - # This requires dependencies to also run on tag pipeline - # However version tag comes with a version commit - # Dependencies must not run on the version commit - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+-.*[0-9]+$/ - -integration:merge: - stage: integration - needs: - - build:merge - - job: build:linux - optional: true - - job: build:windows - optional: true - - job: build:macos - optional: true - # Requires mutual exclusion - resource_group: integration:merge - allow_failure: true - variables: - # Ensure that CI/CD is fetching all commits - # this is necessary to checkout origin/master - # and to also merge origin/staging - GIT_DEPTH: 0 - script: - - > - nix-shell --arg ci true --run $' - printf "Pipeline Succeeded on ${CI_PIPELINE_ID} for ${CI_COMMIT_SHA}\n\n${CI_PIPELINE_URL}" \ - | gh pr comment staging \ - --body-file - \ - --repo "$GH_PROJECT_PATH"; - ' - - git remote add upstream "$GH_PROJECT_URL" - - git checkout origin/master - # Merge up to the current commit (not the latest commit) - - git merge --ff-only "$CI_COMMIT_SHA" - - git push upstream HEAD:master - rules: - # Runs on staging commits and ignores version commits - - if: $CI_COMMIT_BRANCH == 'staging' && $CI_COMMIT_TITLE !~ /^[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - # Runs on tag pipeline where the tag is a prerelease or release version - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(?:-.*[0-9]+)?$/ - -release:distribution: - stage: release - needs: - - build:dist - - build:linux - - build:windows - - build:macos - - integration:merge - # Don't interrupt publishing job - interruptible: false - script: - - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ./.npmrc - - echo 'Publishing library' - - > - nix-shell --arg ci true --run $' - npm publish --access public; - ' - after_script: - - rm -f ./.npmrc - rules: - # Only runs on tag pipeline where the tag is a release version - # This requires dependencies to also run on tag pipeline - # However version tag comes with a version commit - # Dependencies must not run on the version commit - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/ diff --git a/README.md b/README.md index b8f33ca..72b2307 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # js-rpc -staging:[![pipeline status](https://gitlab.com/MatrixAI/open-source/js-rpc/badges/staging/pipeline.svg)](https://gitlab.com/MatrixAI/open-source/js-rpc/commits/staging) -master:[![pipeline status](https://gitlab.com/MatrixAI/open-source/js-rpc/badges/master/pipeline.svg)](https://gitlab.com/MatrixAI/open-source/js-rpc/commits/master) - ## Installation ```sh diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..b2c4339 --- /dev/null +++ b/flake.lock @@ -0,0 +1,78 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1716357214, + "narHash": "sha256-gQh7A8QOJLUhO7bdtQ8ZW9/KM70ciKskxSYgC1Lzm6g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e69e710edfed397959507bcee120ec8a9c7ff03e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3f33387a5c85d94b305062a4f97d5b2899094efa", + "type": "github" + } + }, + "nixpkgs-matrix": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1727848059, + "narHash": "sha256-ClOH2O/IMe3x8F735//QVIQVIPeTik9Fjn0P0n7JLYg=", + "owner": "MatrixAI", + "repo": "nixpkgs-matrix", + "rev": "4df49ebd2f7c72195ea9e2414e9a968693c68600", + "type": "github" + }, + "original": { + "id": "nixpkgs-matrix", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs-matrix": "nixpkgs-matrix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..07a7058 --- /dev/null +++ b/flake.nix @@ -0,0 +1,47 @@ +{ + inputs = { + nixpkgs-matrix = { + type = "indirect"; + id = "nixpkgs-matrix"; + inputs.nixpkgs.url = + "github:NixOS/nixpkgs?rev=e69e710edfed397959507bcee120ec8a9c7ff03e"; + }; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs-matrix, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs-matrix.legacyPackages.${system}; + + shell = { ci ? false }: + with pkgs; + mkShell { + nativeBuildInputs = [ nodejs_20 shellcheck gitAndTools.gh ]; + NIX_DONT_SET_RPATH = true; + NIX_NO_SELF_RPATH = true; + shellHook = '' + echo "Entering $(npm pkg get name)" + set -o allexport + . ./.env + set +o allexport + set -v + ${lib.optionalString ci '' + set -o errexit + set -o nounset + set -o pipefail + shopt -s inherit_errexit + ''} + mkdir --parents "$(pwd)/tmp" + export PATH="$(pwd)/dist/bin:$(npm root)/.bin:$PATH" + npm install --ignore-scripts + set +v + ''; + }; + in { + devShells = { + default = shell { ci = false; }; + ci = shell { ci = true; }; + }; + }); +} diff --git a/pkgs.nix b/pkgs.nix deleted file mode 100644 index 2997ae2..0000000 --- a/pkgs.nix +++ /dev/null @@ -1,4 +0,0 @@ -import ( - let rev = "ea5234e7073d5f44728c499192544a84244bf35a"; in - builtins.fetchTarball "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz" -) diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 54ec565..0000000 --- a/shell.nix +++ /dev/null @@ -1,34 +0,0 @@ -{ pkgs ? import ./pkgs.nix {}, ci ? false }: - -with pkgs; -mkShell { - nativeBuildInputs = [ - nodejs_20 - shellcheck - gitAndTools.gh - ]; - shellHook = '' - echo "Entering $(npm pkg get name)" - set -o allexport - . ./.env - set +o allexport - set -v - ${ - lib.optionalString ci - '' - set -o errexit - set -o nounset - set -o pipefail - shopt -s inherit_errexit - '' - } - mkdir --parents "$(pwd)/tmp" - - # Built executables and NPM executables - export PATH="$(pwd)/dist/bin:$(npm root)/.bin:$PATH" - - npm install --ignore-scripts - - set +v - ''; -}