├── .changeset ├── README.md └── config.json ├── .codesandbox └── ci.json ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ ├── release.yml │ └── size-limit.yml ├── .gitignore ├── .lintstagedrc.js ├── .nvmrc ├── .prettierignore ├── .renovaterc ├── .simple-git-hooks.js ├── .yarn ├── plugins │ ├── @yarnpkg │ │ └── plugin-interactive-tools.cjs │ └── plugin-prepare-lifecycle.cjs └── releases │ └── yarn-3.6.4.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __mocks__ ├── prettier.ts └── tinyexec.ts ├── img └── demo.gif ├── package.json ├── shim.d.ts ├── src ├── cli.mts ├── createIgnorer.ts ├── createMatcher.ts ├── index.ts ├── isSupportedExtension.ts ├── processFiles.ts ├── scms │ ├── git.ts │ ├── hg.ts │ └── index.ts ├── tsconfig.json ├── types.ts └── utils.ts ├── test ├── isSupportedExtension.spec.ts ├── pattern.spec.ts ├── pretty-quick.spec.ts ├── scm-git.spec.ts └── scm-hg.spec.ts ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.lib.json └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "prettier/pretty-quick" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [], 11 | "linked": [], 12 | "access": "public", 13 | "baseBranch": "master", 14 | "updateInternalDependencies": "patch", 15 | "ignore": [] 16 | } 17 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "18", 3 | "sandboxes": [] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | indent_style=space 5 | indent_size=2 6 | tab_width=2 7 | end_of_line=lf 8 | charset=utf-8 9 | trim_trailing_whitespace=true 10 | insert_final_newline=true 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - azz 3 | - JounQin 4 | - 1stG 5 | - rx-ts 6 | - un-ts 7 | patreon: 1stG 8 | open_collective: unts 9 | custom: 10 | - https://opencollective.com/1stG 11 | - https://opencollective.com/rxts 12 | - https://afdian.net/@JounQin 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | ci: 13 | name: Lint and Test with Node.js ${{ matrix.node }} on ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | node: 17 | - 14 18 | - 16 19 | - 18 20 | - 20 21 | - 22 22 | os: 23 | - ubuntu-latest 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - name: Checkout Repo 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Node.js ${{ matrix.node }} 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node }} 33 | cache: yarn 34 | 35 | - name: Install Dependencies 36 | run: yarn --immutable 37 | 38 | - name: Build and Test 39 | run: yarn run-s build test 40 | 41 | - name: Lint 42 | run: yarn lint 43 | if: matrix.node != 14 && matrix.node != 16 44 | env: 45 | EFF_NO_LINK_RULES: true 46 | PARSER_NO_WATCH: true 47 | 48 | - name: Codecov 49 | uses: codecov/codecov-action@v3 50 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: '0 2 * * 3' 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: 26 | - javascript 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v2 34 | with: 35 | languages: ${{ matrix.language }} 36 | queries: +security-and-quality 37 | 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@v2 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v2 43 | with: 44 | category: '/language:${{ matrix.language }}' 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v4 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js LTS 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | cache: yarn 24 | 25 | - name: Install Dependencies 26 | run: yarn --immutable 27 | 28 | - name: Build 29 | run: yarn build 30 | 31 | - name: Create Release Pull Request or Publish to npm 32 | uses: changesets/action@v1 33 | with: 34 | commit: 'chore: release pretty-quick' 35 | title: 'chore: release pretty-quick' 36 | publish: yarn release 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: Size Limit 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | size-limit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup Node.js LTS 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: lts/* 18 | cache: yarn 19 | 20 | - name: Install Dependencies 21 | run: yarn --immutable 22 | 23 | - uses: andresz1/size-limit-action@v1 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | skip_step: install 27 | script: yarn size-limit --json 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | *.log 4 | /.yarn/* 5 | !/.yarn/plugins 6 | !/.yarn/releases 7 | coverage 8 | .eslintcache 9 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@1stg/lint-staged/tsc') 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib 3 | /.yarn 4 | -------------------------------------------------------------------------------- /.renovaterc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>1stG/configs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.simple-git-hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@1stg/simple-git-hooks') 2 | -------------------------------------------------------------------------------- /.yarn/plugins/plugin-prepare-lifecycle.cjs: -------------------------------------------------------------------------------- 1 | module.exports={name:"plugin-prepare-lifecycle",factory:e=>({hooks:{afterAllInstalled(r){if(!r.topLevelWorkspace.manifest.scripts.get("prepare"))return;e("@yarnpkg/shell").execute("yarn prepare")}}})}; 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: true 4 | 5 | enableTelemetry: false 6 | 7 | nodeLinker: node-modules 8 | 9 | plugins: 10 | - path: .yarn/plugins/plugin-prepare-lifecycle.cjs 11 | spec: "https://github.com/un-es/yarn-plugin-prepare-lifecycle/releases/download/v0.0.1/index.js" 12 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 13 | spec: "@yarnpkg/plugin-interactive-tools" 14 | 15 | yarnPath: .yarn/releases/yarn-3.6.4.cjs 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pretty-quick 2 | 3 | ## 4.1.1 4 | 5 | ### Patch Changes 6 | 7 | - [#200](https://github.com/prettier/pretty-quick/pull/200) [`40b2e55`](https://github.com/prettier/pretty-quick/commit/40b2e55aa45c036d3f1650e2862e1ca0bc186469) Thanks [@ConradHughes](https://github.com/ConradHughes)! - fix: directory symlink handling - close #196 8 | 9 | ## 4.1.0 10 | 11 | ### Minor Changes 12 | 13 | - [#195](https://github.com/prettier/pretty-quick/pull/195) [`dcf5da4`](https://github.com/prettier/pretty-quick/commit/dcf5da46ce517547077f5b2d9b0519e6676361d7) Thanks [@pralkarz](https://github.com/pralkarz)! - feat: replace `execa` with `tinyexec` 14 | 15 | - [#198](https://github.com/prettier/pretty-quick/pull/198) [`76c5371`](https://github.com/prettier/pretty-quick/commit/76c5371adbf7956e9514592020759c9cdb16945f) Thanks [@JounQin](https://github.com/JounQin)! - chore: bump upgradable deps without breaking changes 16 | 17 | ## 4.0.0 18 | 19 | ### Major Changes 20 | 21 | - [#182](https://github.com/prettier/pretty-quick/pull/182) [`f1cacb2`](https://github.com/prettier/pretty-quick/commit/f1cacb21c3a69cb50c34b716f59db4d746849c60) Thanks [@JounQin](https://github.com/JounQin)! - feat!: support prettier v3 22 | 23 | ## 3.3.1 24 | 25 | ### Patch Changes 26 | 27 | - [#185](https://github.com/prettier/pretty-quick/pull/185) [`ca4d269`](https://github.com/prettier/pretty-quick/commit/ca4d269328fa6cbca651060f9af5a7e48c3abc02) Thanks [@JounQin](https://github.com/JounQin)! - fix: remove unexpected .tsbuildinfo file 28 | 29 | ## 3.3.0 30 | 31 | ### Minor Changes 32 | 33 | - [#180](https://github.com/prettier/pretty-quick/pull/180) [`93924ab`](https://github.com/prettier/pretty-quick/commit/93924ab962ea94cc21f0ea6464a01b9f65543eb6) Thanks [@SukkaW](https://github.com/SukkaW)! - refactor: replace `chalk` and `multimatch` with their lightweight and performant alternatives 34 | 35 | ### Patch Changes 36 | 37 | - [#183](https://github.com/prettier/pretty-quick/pull/183) [`71aab56`](https://github.com/prettier/pretty-quick/commit/71aab568495773cb3b9a683b79e6f20294ed585a) Thanks [@JounQin](https://github.com/JounQin)! - fix: more robust computation of git directory 38 | 39 | ## 3.2.1 40 | 41 | ### Patch Changes 42 | 43 | - [#178](https://github.com/prettier/pretty-quick/pull/178) [`1929cc9`](https://github.com/prettier/pretty-quick/commit/1929cc96fe0210c6fc44015c996ccd9c7eabd7fc) Thanks [@JounQin](https://github.com/JounQin)! - fix: add tslib as dependency 44 | 45 | ## 3.2.0 46 | 47 | ### Minor Changes 48 | 49 | - [#175](https://github.com/prettier/pretty-quick/pull/175) [`4f5a345`](https://github.com/prettier/pretty-quick/commit/4f5a345f932a33c99da54f38dfbf78f2b23ae773) Thanks [@JounQin](https://github.com/JounQin)! - feat: migrate code base to TypeScript 50 | 51 | ## 3.1.4 52 | 53 | ### Patch Changes 54 | 55 | - [#172](https://github.com/prettier/pretty-quick/pull/172) [`49acad2`](https://github.com/prettier/pretty-quick/commit/49acad2abcf327a892eee0cef5d96ec94788414a) Thanks [@JounQin](https://github.com/JounQin)! - fix: incorrect prettier peer version 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017- Lucas Azzola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pretty-quick` 2 | 3 | [![GitHub Actions](https://github.com/prettier/pretty-quick/workflows/CI/badge.svg)](https://github.com/prettier/pretty-quick/actions/workflows/ci.yml) 4 | [![Codecov](https://img.shields.io/codecov/c/github/prettier/pretty-quick.svg)](https://codecov.io/gh/prettier/pretty-quick) 5 | [![type-coverage](https://img.shields.io/badge/dynamic/json.svg?label=type-coverage&prefix=%E2%89%A5&suffix=%&query=$.typeCoverage.atLeast&uri=https%3A%2F%2Fraw.githubusercontent.com%2Fun-ts%2Flib-boilerplate%2Fmain%2Fpackage.json)](https://github.com/plantain-00/type-coverage) 6 | [![npm](https://img.shields.io/npm/v/pretty-quick.svg)](https://www.npmjs.com/package/pretty-quick) 7 | [![GitHub Release](https://img.shields.io/github/release/prettier/pretty-quick)](https://github.com/prettier/pretty-quick/releases) 8 | 9 | [![Conventional Commits](https://img.shields.io/badge/conventional%20commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 10 | [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com) 11 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 12 | [![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 13 | [![changesets](https://img.shields.io/badge/maintained%20with-changesets-176de3.svg)](https://github.com/changesets/changesets) 14 | 15 | > Get Pretty Quick 16 | 17 | Runs [Prettier](https://prettier.io) on your changed files. 18 | 19 | ![demo](./img/demo.gif) 20 | 21 | Supported source control managers: 22 | 23 | - Git 24 | - Mercurial 25 | 26 | ## Install 27 | 28 | ```sh 29 | # npm 30 | npm install -D prettier pretty-quick 31 | ``` 32 | 33 | ```sh 34 | # yarn 35 | yarn add -D prettier pretty-quick 36 | ``` 37 | 38 | ## Usage 39 | 40 | ```sh 41 | # npx 42 | npx pretty-quick 43 | 44 | # yarn 45 | yarn pretty-quick 46 | ``` 47 | 48 | ## Pre-Commit Hook 49 | 50 | You can run `pretty-quick` as a `pre-commit` hook using [`simple-git-hooks`](https://github.com/toplenboren/simple-git-hooks). 51 | 52 | ```sh 53 | # npm 54 | npm install -D simple-git-hooks 55 | 56 | # yarn 57 | yarn add -D simple-git-hooks 58 | ``` 59 | 60 | In `package.json`, add: 61 | 62 | ```jsonc 63 | "simple-git-hooks": { 64 | "pre-commit": "yarn pretty-quick --staged" // or "npx pretty-quick --staged" 65 | } 66 | ``` 67 | 68 | ## CLI Flags 69 | 70 | ### `--staged` (only git) 71 | 72 | Pre-commit mode. Under this flag only staged files will be formatted, and they will be re-staged after formatting. 73 | 74 | Partially staged files will not be re-staged after formatting and pretty-quick will exit with a non-zero exit code. The intent is to abort the git commit and allow the user to amend their selective staging to include formatting fixes. 75 | 76 | ### `--no-restage` (only git) 77 | 78 | Use with the `--staged` flag to skip re-staging files after formatting. 79 | 80 | ### `--branch` 81 | 82 | When not in `staged` pre-commit mode, use this flag to compare changes with the specified branch. Defaults to `master` (git) / `default` (hg) branch. 83 | 84 | ### `--pattern` 85 | 86 | Filters the files for the given [minimatch](https://github.com/isaacs/minimatch) pattern. 87 | For example `pretty-quick --pattern "**/*.*(js|jsx)"` or `pretty-quick --pattern "**/*.js" --pattern "**/*.jsx"` 88 | 89 | ### `--verbose` 90 | 91 | Outputs the name of each file right before it is processed. This can be useful if Prettier throws an error and you can't identify which file is causing the problem. 92 | 93 | ### `--bail` 94 | 95 | Prevent `git commit` if any files are fixed. 96 | 97 | ### `--check` 98 | 99 | Check that files are correctly formatted, but don't format them. This is useful on CI to verify that all changed files in the current branch were correctly formatted. 100 | 101 | ### `--no-resolve-config` 102 | 103 | Do not resolve prettier config when determining which files to format, just use standard set of supported file types & extensions prettier supports. This may be useful if you do not need any customization and see performance issues. 104 | 105 | By default, pretty-quick will check your prettier configuration file for any overrides you define to support formatting of additional file extensions. 106 | 107 | Example `.prettierrc` file to support formatting files with `.cmp` or `.page` extensions as html. 108 | 109 | ```json 110 | { 111 | "printWidth": 120, 112 | "bracketSpacing": false, 113 | "overrides": [ 114 | { 115 | "files": "*.{cmp,page}", 116 | "options": { "parser": "html" } 117 | } 118 | ] 119 | } 120 | ``` 121 | 122 | 135 | 136 | ### `--ignore-path` 137 | 138 | Check an alternative file for ignoring files with the same format as [`.prettierignore`](https://prettier.io/docs/en/ignore#ignoring-files). 139 | For example `pretty-quick --ignore-path .gitignore` 140 | 141 | ## Configuration and Ignore Files 142 | 143 | `pretty-quick` will respect your [`.prettierrc`](https://prettier.io/docs/en/configuration), [`.prettierignore`](https://prettier.io/docs/en/ignore#ignoring-files), and [`.editorconfig`](http://editorconfig.org/) files if you don't use `--ignore-path` . Configuration files will be found by searching up the file system. `.prettierignore` files are only found from the repository root and the working directory that the command was executed from. 144 | -------------------------------------------------------------------------------- /__mocks__/prettier.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/require-await */ 2 | 3 | import path from 'path' 4 | 5 | export const format = jest.fn(async (input: string) => 'formatted:' + input) 6 | 7 | export const resolveConfig = jest.fn(async (file: string) => ({ file })) 8 | 9 | export const getFileInfo = jest.fn(async (file: string) => { 10 | const ext = path.extname(file) 11 | return { 12 | ignored: false, 13 | inferredParser: ext === '.js' || ext === '.md' ? 'babel' : null, 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /__mocks__/tinyexec.ts: -------------------------------------------------------------------------------- 1 | export const exec = jest.fn().mockReturnValue({ 2 | stdout: '', 3 | stderr: '', 4 | // eslint-disable-next-line @typescript-eslint/no-empty-function 5 | kill: () => {}, 6 | }) 7 | -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prettier/pretty-quick/f8b9eb87654a9420c4fb6cb9d47ac6054edf5662/img/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pretty-quick", 3 | "version": "4.1.1", 4 | "description": "Get Pretty Quick", 5 | "repository": "prettier/pretty-quick", 6 | "author": "Lucas Azzola <@azz>", 7 | "maintainers": [ 8 | "JounQin (https://www.1stG.me) " 9 | ], 10 | "license": "MIT", 11 | "packageManager": "yarn@3.6.4", 12 | "engines": { 13 | "node": ">=14" 14 | }, 15 | "bin": "lib/cli.mjs", 16 | "main": "lib/index.js", 17 | "module": "lib/index.esm.mjs", 18 | "exports": { 19 | ".": { 20 | "types": "./lib/index.d.ts", 21 | "require": "./lib/index.js", 22 | "default": "./lib/index.esm.mjs" 23 | }, 24 | "./*": "./lib/*.js", 25 | "./package.json": "./package.json" 26 | }, 27 | "types": "lib/index.d.ts", 28 | "files": [ 29 | "bin", 30 | "img", 31 | "lib", 32 | "!**/*.tsbuildinfo" 33 | ], 34 | "keywords": [ 35 | "git", 36 | "mercurial", 37 | "hg", 38 | "prettier", 39 | "pretty-quick", 40 | "formatting", 41 | "code", 42 | "vcs", 43 | "precommit" 44 | ], 45 | "scripts": { 46 | "build": "run-p 'build:*'", 47 | "build:r": "r -f esm", 48 | "build:tsc": "tsc -b", 49 | "lint": "run-p 'lint:*'", 50 | "lint:es": "eslint . --cache", 51 | "lint:tsc": "tsc --noEmit", 52 | "prepare": "patch-package && simple-git-hooks", 53 | "release": "yarn build && clean-pkg-json && changeset publish", 54 | "test": "jest" 55 | }, 56 | "peerDependencies": { 57 | "prettier": "^3.0.0" 58 | }, 59 | "dependencies": { 60 | "find-up": "^5.0.0", 61 | "ignore": "^7.0.3", 62 | "mri": "^1.2.0", 63 | "picocolors": "^1.1.1", 64 | "picomatch": "^4.0.2", 65 | "tinyexec": "^0.3.2", 66 | "tslib": "^2.8.1" 67 | }, 68 | "devDependencies": { 69 | "@1stg/lib-config": "^13.0.0", 70 | "@changesets/changelog-github": "^0.5.1", 71 | "@changesets/cli": "^2.28.1", 72 | "@commitlint/cli": "^18.6.1", 73 | "@pkgr/rollup": "^6.0.0", 74 | "@total-typescript/ts-reset": "^0.6.1", 75 | "@types/jest": "^29.5.14", 76 | "@types/mock-fs": "^4.13.4", 77 | "@types/picomatch": "^3.0.2", 78 | "@unts/patch-package": "^8.1.1", 79 | "clean-pkg-json": "^1.2.0", 80 | "eslint": "^8.57.1", 81 | "jest": "^29.7.0", 82 | "lint-staged": "^15.4.3", 83 | "mock-fs": "^5.5.0", 84 | "npm-run-all": "^4.1.5", 85 | "prettier": "^3.5.3", 86 | "pretty-quick": "link:.", 87 | "simple-git-hooks": "^2.9.0", 88 | "size-limit": "^11.2.0", 89 | "size-limit-preset-node-lib": "^0.3.0", 90 | "ts-jest": "^29.2.6", 91 | "ts-node": "^10.9.2", 92 | "typescript": "^5.8.2" 93 | }, 94 | "resolutions": { 95 | "prettier": "^3.5.3", 96 | "rollup": "^3.29.5" 97 | }, 98 | "commitlint": { 99 | "extends": "@1stg" 100 | }, 101 | "eslintConfig": { 102 | "extends": "@1stg", 103 | "rules": { 104 | "unicorn/prefer-node-protocol": "off" 105 | }, 106 | "overrides": [ 107 | { 108 | "files": "__mocks__/*.*", 109 | "env": { 110 | "jest": true 111 | } 112 | } 113 | ] 114 | }, 115 | "eslintIgnore": [ 116 | "coverage", 117 | "lib", 118 | "!/.*.js" 119 | ], 120 | "jest": { 121 | "preset": "ts-jest", 122 | "testMatch": [ 123 | "/test/*.spec.ts" 124 | ], 125 | "collectCoverage": true, 126 | "moduleNameMapper": { 127 | "^(\\.{1,2}/.*)\\.js$": "$1", 128 | "^pretty-quick$": "/src", 129 | "^pretty-quick/(.+)$": "/src/$1" 130 | } 131 | }, 132 | "prettier": "@1stg/prettier-config", 133 | "size-limit": [ 134 | { 135 | "path": "src/index.ts", 136 | "limit": "1.5KB" 137 | } 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /shim.d.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset' 2 | -------------------------------------------------------------------------------- /src/cli.mts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import mri from 'mri' 4 | import picocolors from 'picocolors' 5 | 6 | import prettyQuick from './index.js' 7 | 8 | const args = mri(process.argv.slice(2), { 9 | alias: { 10 | 'resolve-config': 'resolveConfig', 11 | 'ignore-path': 'ignorePath', 12 | }, 13 | }) 14 | 15 | const main = async () => { 16 | const prettyQuickResult = await prettyQuick(process.cwd(), { 17 | ...args, 18 | onFoundSinceRevision: (scm, revision) => { 19 | console.log( 20 | `🔍 Finding changed files since ${picocolors.bold( 21 | scm, 22 | )} revision ${picocolors.bold(revision)}.`, 23 | ) 24 | }, 25 | 26 | onFoundChangedFiles: changedFiles => { 27 | console.log( 28 | `🎯 Found ${picocolors.bold(changedFiles.length)} changed ${ 29 | changedFiles.length === 1 ? 'file' : 'files' 30 | }.`, 31 | ) 32 | }, 33 | 34 | onPartiallyStagedFile: file => { 35 | console.log( 36 | `✗ Found ${picocolors.bold('partially')} staged file ${file}.`, 37 | ) 38 | }, 39 | 40 | onWriteFile: file => { 41 | console.log(`✍️ Fixing up ${picocolors.bold(file)}.`) 42 | }, 43 | 44 | onCheckFile: (file, isFormatted) => { 45 | if (!isFormatted) { 46 | console.log(`⛔️ Check failed: ${picocolors.bold(file)}`) 47 | } 48 | }, 49 | 50 | onExamineFile: file => { 51 | console.log(`🔍 Examining ${picocolors.bold(file)}.`) 52 | }, 53 | }) 54 | 55 | if (prettyQuickResult.success) { 56 | console.log('✅ Everything is awesome!') 57 | } else { 58 | if (prettyQuickResult.errors.includes('PARTIALLY_STAGED_FILE')) { 59 | console.log( 60 | '✗ Partially staged files were fixed up.' + 61 | ` ${picocolors.bold('Please update stage before committing')}.`, 62 | ) 63 | } 64 | if (prettyQuickResult.errors.includes('BAIL_ON_WRITE')) { 65 | console.log( 66 | '✗ File had to be prettified and prettyQuick was set to bail mode.', 67 | ) 68 | } 69 | if (prettyQuickResult.errors.includes('CHECK_FAILED')) { 70 | console.log( 71 | '✗ Code style issues found in the above file(s). Forgot to run Prettier?', 72 | ) 73 | } 74 | // eslint-disable-next-line n/no-process-exit 75 | process.exit(1) // ensure git hooks abort 76 | } 77 | } 78 | 79 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 80 | main() 81 | -------------------------------------------------------------------------------- /src/createIgnorer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/filename-case */ 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | 6 | import ignore from 'ignore' 7 | 8 | export default (directory: string, filename = '.prettierignore') => { 9 | const file = path.join(directory, filename) 10 | 11 | if (fs.existsSync(file)) { 12 | const text = fs.readFileSync(file, 'utf8') 13 | const filter = ignore().add(text).createFilter() 14 | return (filepath: string) => filter(path.join(filepath)) 15 | } 16 | 17 | return () => true 18 | } 19 | -------------------------------------------------------------------------------- /src/createMatcher.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/filename-case */ 2 | 3 | import path from 'path' 4 | 5 | import picomatch from 'picomatch' 6 | 7 | export default (pattern: string[] | string | undefined) => { 8 | // Match everything if no pattern was given 9 | if (typeof pattern !== 'string' && !Array.isArray(pattern)) { 10 | return () => true 11 | } 12 | const patterns = Array.isArray(pattern) ? pattern : [pattern] 13 | 14 | const isMatch = picomatch(patterns, { dot: true }) 15 | return (file: string) => isMatch(path.normalize(file)) 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import createIgnorer from './createIgnorer.js' 2 | import createMatcher from './createMatcher.js' 3 | import isSupportedExtension from './isSupportedExtension.js' 4 | import processFiles from './processFiles.js' 5 | import scms from './scms/index.js' 6 | import { PrettyQuickOptions } from './types.js' 7 | import { filterAsync } from './utils.js' 8 | 9 | export = async ( 10 | currentDirectory: string, 11 | { 12 | config, 13 | since, 14 | staged, 15 | pattern, 16 | restage = true, 17 | branch, 18 | bail, 19 | check, 20 | ignorePath, 21 | verbose, 22 | onFoundSinceRevision, 23 | onFoundChangedFiles, 24 | onPartiallyStagedFile, 25 | onExamineFile, 26 | onCheckFile, 27 | onWriteFile, 28 | resolveConfig = true, 29 | }: Partial = {}, 30 | // eslint-disable-next-line sonarjs/cognitive-complexity 31 | ) => { 32 | const scm = scms(currentDirectory) 33 | if (!scm) { 34 | throw new Error('Unable to detect a source control manager.') 35 | } 36 | const directory = scm.rootDirectory 37 | 38 | const revision = 39 | since || (await scm.getSinceRevision(directory, { staged, branch })) 40 | 41 | onFoundSinceRevision?.(scm.name, revision) 42 | 43 | const rootIgnorer = createIgnorer(directory, ignorePath) 44 | const cwdIgnorer = 45 | currentDirectory === directory 46 | ? () => true 47 | : createIgnorer(currentDirectory, ignorePath) 48 | 49 | const patternMatcher = createMatcher(pattern) 50 | 51 | const isFileSupportedExtension = isSupportedExtension(resolveConfig) 52 | 53 | const unfilteredChangedFiles = await scm.getChangedFiles( 54 | directory, 55 | revision, 56 | staged, 57 | ) 58 | const changedFiles = await filterAsync( 59 | unfilteredChangedFiles 60 | .filter(patternMatcher) 61 | .filter(rootIgnorer) 62 | .filter(cwdIgnorer), 63 | isFileSupportedExtension, 64 | ) 65 | 66 | const unfilteredStagedFiles = await scm.getUnstagedChangedFiles(directory) 67 | const unstagedFiles = staged 68 | ? await filterAsync( 69 | unfilteredStagedFiles 70 | .filter(patternMatcher) 71 | .filter(rootIgnorer) 72 | .filter(cwdIgnorer), 73 | isFileSupportedExtension, 74 | ) 75 | : [] 76 | 77 | const wasFullyStaged = (file: string) => !unstagedFiles.includes(file) 78 | 79 | onFoundChangedFiles?.(changedFiles) 80 | 81 | const failReasons = new Set() 82 | 83 | await processFiles(directory, changedFiles, { 84 | check, 85 | config, 86 | onWriteFile: async (file: string) => { 87 | await onWriteFile?.(file) 88 | if (bail) { 89 | failReasons.add('BAIL_ON_WRITE') 90 | } 91 | if (staged && restage) { 92 | if (wasFullyStaged(file)) { 93 | await scm.stageFile(directory, file) 94 | } else { 95 | onPartiallyStagedFile?.(file) 96 | failReasons.add('PARTIALLY_STAGED_FILE') 97 | } 98 | } 99 | }, 100 | onCheckFile: (file: string, isFormatted: boolean) => { 101 | onCheckFile?.(file, isFormatted) 102 | if (!isFormatted) { 103 | failReasons.add('CHECK_FAILED') 104 | } 105 | }, 106 | onExamineFile: verbose ? onExamineFile : undefined, 107 | }) 108 | 109 | return { 110 | success: failReasons.size === 0, 111 | errors: [...failReasons], 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/isSupportedExtension.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/filename-case */ 2 | 3 | import fs from 'fs/promises' 4 | 5 | import { 6 | type FileInfoOptions, 7 | getFileInfo, 8 | resolveConfig as prettierResolveConfig, 9 | } from 'prettier' 10 | 11 | export default (resolveConfig?: boolean) => async (file: string) => { 12 | const stat = await fs.stat(file).catch(_error => null) 13 | /* If file exists but is actually a directory, getFileInfo might end up trying 14 | * to open it and read it as a file, so let's not let that happen. On the 15 | * other hand, the tests depend on our not failing out with files that don't 16 | * exist, so permit nonexistent files to go through (they appear to be 17 | * detected by suffix, so never get read). 18 | */ 19 | if (stat?.isDirectory()) { 20 | return false 21 | } 22 | const config = (await prettierResolveConfig(file, { 23 | editorconfig: true, 24 | })) as FileInfoOptions 25 | const fileInfo = await getFileInfo(file, { 26 | resolveConfig, 27 | ...config, 28 | }) 29 | return !!fileInfo.inferredParser 30 | } 31 | -------------------------------------------------------------------------------- /src/processFiles.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/filename-case */ 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | 6 | import { format, check as prettierCheck, resolveConfig } from 'prettier' 7 | 8 | import type { PrettyQuickOptions } from './types.js' 9 | 10 | export default async ( 11 | directory: string, 12 | files: string[], 13 | { 14 | check, 15 | config, 16 | onExamineFile, 17 | onCheckFile, 18 | onWriteFile, 19 | }: Partial = {}, 20 | ) => { 21 | for (const relative of files) { 22 | onExamineFile?.(relative) 23 | const file = path.join(directory, relative) 24 | const options = { 25 | ...(await resolveConfig(file, { 26 | config, 27 | editorconfig: true, 28 | })), 29 | filepath: file, 30 | } 31 | const input = fs.readFileSync(file, 'utf8') 32 | 33 | if (check) { 34 | const isFormatted = await prettierCheck(input, options) 35 | onCheckFile?.(relative, isFormatted) 36 | continue 37 | } 38 | 39 | const output = await format(input, options) 40 | 41 | if (output !== input) { 42 | fs.writeFileSync(file, output) 43 | await onWriteFile?.(relative) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/scms/git.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import findUp from 'find-up' 5 | import { Output, exec } from 'tinyexec' 6 | 7 | export const name = 'git' 8 | 9 | export const detect = (directory: string) => { 10 | if (fs.existsSync(path.join(directory, '.git'))) { 11 | return directory 12 | } 13 | 14 | const gitDirectory = findUp.sync('.git', { 15 | cwd: directory, 16 | type: 'directory', 17 | }) 18 | 19 | const gitWorkTreeFile = findUp.sync('.git', { 20 | cwd: directory, 21 | type: 'file', 22 | }) 23 | 24 | // if both of these are null then return null 25 | if (!gitDirectory && !gitWorkTreeFile) { 26 | return null 27 | } 28 | 29 | // if only one of these exists then return it 30 | if (gitDirectory && !gitWorkTreeFile) { 31 | return path.dirname(gitDirectory) 32 | } 33 | 34 | if (gitWorkTreeFile && !gitDirectory) { 35 | return path.dirname(gitWorkTreeFile) 36 | } 37 | 38 | const gitRepoDirectory = path.dirname(gitDirectory!) 39 | const gitWorkTreeDirectory = path.dirname(gitWorkTreeFile!) 40 | // return the deeper of these two 41 | return gitRepoDirectory.length > gitWorkTreeDirectory.length 42 | ? gitRepoDirectory 43 | : gitWorkTreeDirectory 44 | } 45 | 46 | const runGit = (directory: string, args: string[]) => 47 | exec('git', args, { 48 | nodeOptions: { 49 | cwd: directory, 50 | }, 51 | }) 52 | 53 | const getLines = (tinyexecOutput: Output) => tinyexecOutput.stdout.split('\n') 54 | 55 | export const getSinceRevision = async ( 56 | directory: string, 57 | { staged, branch }: { staged?: boolean; branch?: string }, 58 | ) => { 59 | try { 60 | let revision = 'HEAD' 61 | if (!staged) { 62 | const revisionOutput = await runGit(directory, [ 63 | 'merge-base', 64 | 'HEAD', 65 | branch || 'master', 66 | ]) 67 | revision = revisionOutput.stdout.trim() 68 | } 69 | 70 | const revParseOutput = await runGit(directory, [ 71 | 'rev-parse', 72 | '--short', 73 | revision, 74 | ]) 75 | return revParseOutput.stdout.trim() 76 | } catch (err) { 77 | const error = err as Error 78 | if ( 79 | /HEAD/.test(error.message) || 80 | (staged && /Needed a single revision/.test(error.message)) 81 | ) { 82 | return null 83 | } 84 | throw error 85 | } 86 | } 87 | 88 | export const getChangedFiles = async ( 89 | directory: string, 90 | revision: string | null, 91 | staged?: boolean | undefined, 92 | ) => 93 | [ 94 | ...getLines( 95 | await runGit( 96 | directory, 97 | [ 98 | 'diff', 99 | '--name-only', 100 | staged ? '--cached' : null, 101 | '--diff-filter=ACMRTUB', 102 | revision, 103 | ].filter(Boolean), 104 | ), 105 | ), 106 | ...(staged 107 | ? [] 108 | : getLines( 109 | await runGit(directory, [ 110 | 'ls-files', 111 | '--others', 112 | '--exclude-standard', 113 | ]), 114 | )), 115 | ].filter(Boolean) 116 | 117 | export const getUnstagedChangedFiles = (directory: string) => { 118 | return getChangedFiles(directory, null, false) 119 | } 120 | 121 | export const stageFile = (directory: string, file: string) => 122 | runGit(directory, ['add', file]) 123 | -------------------------------------------------------------------------------- /src/scms/hg.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import findUp from 'find-up' 4 | import { Output, exec } from 'tinyexec' 5 | 6 | export const name = 'hg' 7 | 8 | export const detect = (directory: string) => { 9 | const hgDirectory = findUp.sync('.hg', { 10 | cwd: directory, 11 | type: 'directory', 12 | }) 13 | if (hgDirectory) { 14 | return path.dirname(hgDirectory) 15 | } 16 | } 17 | 18 | const runHg = (directory: string, args: string[]) => 19 | exec('hg', args, { 20 | nodeOptions: { 21 | cwd: directory, 22 | }, 23 | }) 24 | 25 | const getLines = (tinyexecOutput: Output) => tinyexecOutput.stdout.split('\n') 26 | 27 | export const getSinceRevision = async ( 28 | directory: string, 29 | { branch }: { branch?: string }, 30 | ) => { 31 | const revisionOutput = await runHg(directory, [ 32 | 'debugancestor', 33 | 'tip', 34 | branch || 'default', 35 | ]) 36 | const revision = revisionOutput.stdout.trim() 37 | 38 | const hgOutput = await runHg(directory, ['id', '-i', '-r', revision]) 39 | return hgOutput.stdout.trim() 40 | } 41 | 42 | export const getChangedFiles = async ( 43 | directory: string, 44 | revision: string | null, 45 | _staged?: boolean, 46 | ) => 47 | [ 48 | ...getLines( 49 | await runHg(directory, [ 50 | 'status', 51 | '-n', 52 | '-a', 53 | '-m', 54 | ...(revision ? ['--rev', revision] : []), 55 | ]), 56 | ), 57 | ].filter(Boolean) 58 | 59 | export const getUnstagedChangedFiles = () => [] 60 | 61 | export const stageFile = (directory: string, file: string) => 62 | runHg(directory, ['add', file]) 63 | -------------------------------------------------------------------------------- /src/scms/index.ts: -------------------------------------------------------------------------------- 1 | import * as gitScm from './git.js' 2 | import * as hgScm from './hg.js' 3 | 4 | const scms = [gitScm, hgScm] 5 | 6 | export default (directory: string) => { 7 | for (const scm of scms) { 8 | const rootDirectory = scm.detect(directory) 9 | if (rootDirectory) { 10 | return { 11 | rootDirectory, 12 | ...scm, 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.lib", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "../lib" 6 | }, 7 | "include": ["../shim.d.ts", "."] 8 | } 9 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface PrettyQuickOptions { 2 | config: string 3 | since: string 4 | staged: boolean 5 | pattern: string[] | string 6 | restage: boolean 7 | branch: string 8 | bail: boolean 9 | check: boolean 10 | ignorePath: string 11 | resolveConfig: boolean 12 | verbose: boolean 13 | onFoundSinceRevision(name: string, revision: string | null): void 14 | onFoundChangedFiles(changedFiles: string[]): void 15 | onPartiallyStagedFile(file: string): void 16 | onExamineFile(relative: string): void 17 | onCheckFile(relative: string, isFormatted: boolean): void 18 | onWriteFile(relative: string): Promise | void 19 | } 20 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const filterAsync = async ( 2 | items: T[], 3 | predicate: (item: T) => Promise, 4 | ) => { 5 | const boolItems = await Promise.all(items.map(predicate)) 6 | return items.filter((_, i) => boolItems[i]) 7 | } 8 | -------------------------------------------------------------------------------- /test/isSupportedExtension.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/filename-case */ 2 | 3 | import mock from 'mock-fs' 4 | import { getFileInfo } from 'prettier' 5 | 6 | import isSupportedExtension from 'pretty-quick/isSupportedExtension' 7 | 8 | beforeEach(() => { 9 | mock({ 10 | 'banana.js': 'banana()', 11 | 'banana.txt': 'yellow', 12 | 'bsym.js': mock.symlink({ path: 'banana.js' }), 13 | 'bsym.txt': mock.symlink({ path: 'banana.js' }), // Yes extensions don't match 14 | dsym: mock.symlink({ path: 'subdir' }), 15 | subdir: {}, 16 | }) 17 | }) 18 | 19 | afterEach(() => { 20 | mock.restore() 21 | jest.clearAllMocks() 22 | }) 23 | 24 | test('return true when file with supported extension passed in', async () => { 25 | expect(await isSupportedExtension(true)('banana.js')).toEqual(true) 26 | expect(getFileInfo).toHaveBeenCalledWith('banana.js', { 27 | file: 'banana.js', 28 | resolveConfig: true, 29 | }) 30 | }) 31 | 32 | test('return false when file with not supported extension passed in', async () => { 33 | // eslint-disable-next-line sonarjs/no-duplicate-string 34 | expect(await isSupportedExtension(true)('banana.txt')).toEqual(false) 35 | expect(getFileInfo).toHaveBeenCalledWith('banana.txt', { 36 | file: 'banana.txt', 37 | resolveConfig: true, 38 | }) 39 | }) 40 | 41 | test('do not resolve config when false passed', async () => { 42 | expect(await isSupportedExtension(false)('banana.txt')).toEqual(false) 43 | expect(getFileInfo).toHaveBeenCalledWith('banana.txt', { 44 | file: 'banana.txt', 45 | resolveConfig: false, 46 | }) 47 | }) 48 | 49 | test('return true when file symlink with supported extension passed in', async () => { 50 | expect(await isSupportedExtension(true)('bsym.js')).toEqual(true) 51 | expect(getFileInfo).toHaveBeenCalledWith('bsym.js', { 52 | file: 'bsym.js', 53 | resolveConfig: true, 54 | }) 55 | }) 56 | 57 | test('return false when file symlink with unsupported extension passed in', async () => { 58 | expect(await isSupportedExtension(true)('bsym.txt')).toEqual(false) 59 | expect(getFileInfo).toHaveBeenCalledWith('bsym.txt', { 60 | file: 'bsym.txt', 61 | resolveConfig: true, 62 | }) 63 | }) 64 | 65 | test('return false when directory symlink passed in', async () => { 66 | expect(await isSupportedExtension(true)('dsym')).toEqual(false) 67 | expect(getFileInfo).not.toHaveBeenCalled() 68 | }) 69 | -------------------------------------------------------------------------------- /test/pattern.spec.ts: -------------------------------------------------------------------------------- 1 | import mock from 'mock-fs' 2 | import * as tinyexec from 'tinyexec' 3 | 4 | import prettyQuick from 'pretty-quick' 5 | 6 | afterEach(() => { 7 | mock.restore() 8 | jest.clearAllMocks() 9 | }) 10 | 11 | describe('match pattern', () => { 12 | it('issue #73 #125 - regex grouping (round pattern)', async () => { 13 | const onFoundChangedFiles = jest.fn() 14 | 15 | mock({ 16 | '/.git': {}, 17 | '/src/apps/hello/foo.js': "export const foo = 'foo'", 18 | '/src/libs/hello/bar.js': "export const bar = 'bar'", 19 | '/src/tools/hello/baz.js': "export const baz = 'baz'", 20 | '/src/should-not-be-included/hello/zoo.js': "export const zoo = 'zoo'", 21 | }) 22 | 23 | const execSpy = jest.spyOn(tinyexec, 'exec') as jest.Mock 24 | execSpy.mockImplementation((_command: string, args: string[]) => { 25 | switch (args[0]) { 26 | case 'ls-files': { 27 | return { stdout: '' } 28 | } 29 | case 'diff': { 30 | return args[2] === '--cached' 31 | ? { stdout: '' } 32 | : { 33 | stdout: [ 34 | '/src/apps/hello/foo.js', 35 | '/src/libs/hello/bar.js', 36 | '/src/tools/hello/baz.js', 37 | '/src/should-not-be-included/hello/zoo.js', 38 | ].join('\n'), 39 | } 40 | } 41 | default: { 42 | throw new Error(`unexpected arg0: ${args[0]}`) 43 | } 44 | } 45 | }) 46 | 47 | await prettyQuick('root', { 48 | pattern: '**/(apps|libs|tools)/**/*', 49 | since: 'fox', // This is required to prevent `scm.getSinceRevision` call 50 | onFoundChangedFiles, 51 | }) 52 | 53 | expect(onFoundChangedFiles).toHaveBeenCalledWith([ 54 | '/src/apps/hello/foo.js', 55 | '/src/libs/hello/bar.js', 56 | '/src/tools/hello/baz.js', 57 | ]) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/pretty-quick.spec.ts: -------------------------------------------------------------------------------- 1 | import mock from 'mock-fs' 2 | 3 | import prettyQuick from 'pretty-quick' 4 | 5 | jest.mock('execa') 6 | 7 | afterEach(() => mock.restore()) 8 | 9 | test('throws an error when no vcs is found', async () => { 10 | mock({ 11 | 'root/README.md': '', 12 | }) 13 | 14 | await expect(() => prettyQuick('root')).rejects.toThrow( 15 | 'Unable to detect a source control manager.', 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /test/scm-git.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | import mock from 'mock-fs' 4 | import type FileSystem from 'mock-fs/lib/filesystem' 5 | import * as tinyexec from 'tinyexec' 6 | 7 | import prettyQuick from 'pretty-quick' 8 | 9 | const mockGitFs = ( 10 | additionalUnstaged = '', 11 | additionalFiles?: FileSystem.DirectoryItems, 12 | ) => { 13 | mock({ 14 | '/.git': {}, 15 | '/raz.js': 'raz()', 16 | '/foo.js': 'foo()', 17 | '/bar.md': '# foo', 18 | ...additionalFiles, 19 | }) 20 | 21 | const xSpy = jest.spyOn(tinyexec, 'exec') as jest.Mock 22 | xSpy.mockImplementation((command: string, args: string[]) => { 23 | if (command !== 'git') { 24 | throw new Error(`unexpected command: ${command}`) 25 | } 26 | switch (args[0]) { 27 | case 'ls-files': { 28 | return { stdout: '' } 29 | } 30 | case 'diff': { 31 | return args[2] === '--cached' 32 | ? { stdout: './raz.js\n' } 33 | : { stdout: './foo.js\n' + './bar.md\n' + additionalUnstaged } 34 | } 35 | case 'add': { 36 | return { stdout: '' } 37 | } 38 | default: { 39 | throw new Error(`unexpected arg0: ${args[0]}`) 40 | } 41 | } 42 | }) 43 | } 44 | 45 | afterEach(() => { 46 | mock.restore() 47 | jest.clearAllMocks() 48 | }) 49 | 50 | describe('with git', () => { 51 | test('calls `git merge-base`', async () => { 52 | mock({ 53 | '/.git': {}, 54 | }) 55 | 56 | await prettyQuick('root') 57 | 58 | expect(tinyexec.exec).toHaveBeenCalledWith( 59 | 'git', 60 | // eslint-disable-next-line sonarjs/no-duplicate-string 61 | ['merge-base', 'HEAD', 'master'], 62 | { nodeOptions: { cwd: '/' } }, 63 | ) 64 | }) 65 | 66 | test('calls `git merge-base` with root git directory', async () => { 67 | mock({ 68 | '/.git': {}, 69 | '/other-dir': {}, 70 | }) 71 | 72 | await prettyQuick('/other-dir') 73 | 74 | expect(tinyexec.exec).toHaveBeenCalledWith( 75 | 'git', 76 | ['merge-base', 'HEAD', 'master'], 77 | { 78 | nodeOptions: { cwd: '/' }, 79 | }, 80 | ) 81 | }) 82 | 83 | test('with --staged does NOT call `git merge-base`', async () => { 84 | mock({ 85 | '/.git': {}, 86 | }) 87 | 88 | await prettyQuick('root') 89 | 90 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', [ 91 | 'merge-base', 92 | 'HEAD', 93 | 'master', 94 | ]) 95 | }) 96 | 97 | test('with --staged calls diff without revision', async () => { 98 | mock({ 99 | '/.git': {}, 100 | }) 101 | 102 | await prettyQuick('root', { since: 'banana', staged: true }) 103 | 104 | expect(tinyexec.exec).toHaveBeenCalledWith( 105 | 'git', 106 | ['diff', '--name-only', '--diff-filter=ACMRTUB'], 107 | { nodeOptions: { cwd: '/' } }, 108 | ) 109 | }) 110 | 111 | test('calls `git diff --name-only` with revision', async () => { 112 | mock({ 113 | '/.git': {}, 114 | }) 115 | 116 | await prettyQuick('root', { since: 'banana' }) 117 | 118 | expect(tinyexec.exec).toHaveBeenCalledWith( 119 | 'git', 120 | ['diff', '--name-only', '--diff-filter=ACMRTUB', 'banana'], 121 | { nodeOptions: { cwd: '/' } }, 122 | ) 123 | }) 124 | 125 | test('calls `git ls-files`', async () => { 126 | mock({ 127 | '/.git': {}, 128 | }) 129 | 130 | await prettyQuick('root', { since: 'banana' }) 131 | 132 | expect(tinyexec.exec).toHaveBeenCalledWith( 133 | 'git', 134 | ['ls-files', '--others', '--exclude-standard'], 135 | { nodeOptions: { cwd: '/' } }, 136 | ) 137 | }) 138 | 139 | test('calls onFoundSinceRevision with return value from `git merge-base`', async () => { 140 | const onFoundSinceRevision = jest.fn() 141 | 142 | mock({ 143 | '/.git': {}, 144 | }) 145 | 146 | const xSpy = jest.spyOn(tinyexec, 'exec') as jest.Mock 147 | xSpy.mockImplementation(() => ({ stdout: 'banana' })) 148 | 149 | await prettyQuick('root', { onFoundSinceRevision }) 150 | 151 | expect(onFoundSinceRevision).toHaveBeenCalledWith('git', 'banana') 152 | }) 153 | 154 | test('calls onFoundChangedFiles with changed files', async () => { 155 | const onFoundChangedFiles = jest.fn() 156 | mockGitFs() 157 | await prettyQuick('root', { since: 'banana', onFoundChangedFiles }) 158 | expect(onFoundChangedFiles).toHaveBeenCalledWith(['./foo.js', './bar.md']) 159 | }) 160 | 161 | test('calls onWriteFile with changed files', async () => { 162 | const onWriteFile = jest.fn() 163 | mockGitFs() 164 | await prettyQuick('root', { since: 'banana', onWriteFile }) 165 | expect(onWriteFile).toHaveBeenCalledWith('./foo.js') 166 | expect(onWriteFile).toHaveBeenCalledWith('./bar.md') 167 | expect(onWriteFile.mock.calls.length).toBe(2) 168 | }) 169 | 170 | test('calls onWriteFile with changed files for the given pattern', async () => { 171 | const onWriteFile = jest.fn() 172 | mockGitFs() 173 | await prettyQuick('root', { pattern: '*.md', since: 'banana', onWriteFile }) 174 | expect(onWriteFile.mock.calls).toEqual([['./bar.md']]) 175 | }) 176 | 177 | test('calls onWriteFile with changed files for the given globstar pattern', async () => { 178 | const onWriteFile = jest.fn() 179 | mockGitFs() 180 | await prettyQuick('root', { 181 | pattern: '**/*.md', 182 | since: 'banana', 183 | onWriteFile, 184 | }) 185 | expect(onWriteFile.mock.calls).toEqual([['./bar.md']]) 186 | }) 187 | 188 | test('calls onWriteFile with changed files for the given extglob pattern', async () => { 189 | const onWriteFile = jest.fn() 190 | mockGitFs() 191 | await prettyQuick('root', { 192 | pattern: '*.*(md|foo|bar)', 193 | since: 'banana', 194 | onWriteFile, 195 | }) 196 | expect(onWriteFile.mock.calls).toEqual([['./bar.md']]) 197 | }) 198 | 199 | test('calls onWriteFile with changed files for an array of globstar patterns', async () => { 200 | const onWriteFile = jest.fn() 201 | mockGitFs() 202 | await prettyQuick('root', { 203 | pattern: ['**/*.foo', '**/*.md', '**/*.bar'], 204 | since: 'banana', 205 | onWriteFile, 206 | }) 207 | expect(onWriteFile.mock.calls).toEqual([['./bar.md']]) 208 | }) 209 | 210 | test('writes formatted files to disk', async () => { 211 | const onWriteFile = jest.fn() 212 | mockGitFs() 213 | await prettyQuick('root', { since: 'banana', onWriteFile }) 214 | expect(fs.readFileSync('/foo.js', 'utf8')).toEqual('formatted:foo()') 215 | expect(fs.readFileSync('/bar.md', 'utf8')).toEqual('formatted:# foo') 216 | }) 217 | 218 | test('succeeds if a file was changed and bail is not set', async () => { 219 | mockGitFs() 220 | const result = await prettyQuick('root', { since: 'banana' }) 221 | expect(result).toEqual({ errors: [], success: true }) 222 | }) 223 | 224 | test('fails if a file was changed and bail is set to true', async () => { 225 | mockGitFs() 226 | const result = await prettyQuick('root', { since: 'banana', bail: true }) 227 | expect(result).toEqual({ errors: ['BAIL_ON_WRITE'], success: false }) 228 | }) 229 | 230 | test('with --staged stages fully-staged files', async () => { 231 | mockGitFs() 232 | await prettyQuick('root', { since: 'banana', staged: true }) 233 | expect(tinyexec.exec).toHaveBeenCalledWith('git', ['add', './raz.js'], { 234 | nodeOptions: { cwd: '/' }, 235 | }) 236 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { 237 | nodeOptions: { cwd: '/' }, 238 | }) 239 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { 240 | nodeOptions: { cwd: '/' }, 241 | }) 242 | }) 243 | 244 | test('with --staged AND --no-restage does not re-stage any files', async () => { 245 | mockGitFs() 246 | await prettyQuick('root', { since: 'banana', staged: true, restage: false }) 247 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', ['add', './raz.js'], { 248 | cwd: '/', 249 | }) 250 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { 251 | cwd: '/', 252 | }) 253 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { 254 | cwd: '/', 255 | }) 256 | }) 257 | 258 | test('with --staged does not stage previously partially staged files AND aborts commit', async () => { 259 | const additionalUnstaged = './raz.js\n' // raz.js is partly staged and partly not staged 260 | mockGitFs(additionalUnstaged) 261 | await prettyQuick('root', { since: 'banana', staged: true }) 262 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', ['add', './raz.js'], { 263 | cwd: '/', 264 | }) 265 | }) 266 | 267 | test('with --staged returns false', async () => { 268 | const additionalUnstaged = './raz.js\n' // raz.js is partly staged and partly not staged 269 | mockGitFs(additionalUnstaged) 270 | const result = await prettyQuick('root', { since: 'banana', staged: true }) 271 | expect(result).toEqual({ 272 | errors: ['PARTIALLY_STAGED_FILE'], 273 | success: false, 274 | }) 275 | }) 276 | 277 | test('without --staged does NOT stage changed files', async () => { 278 | mockGitFs() 279 | await prettyQuick('root', { since: 'banana' }) 280 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { 281 | cwd: '/', 282 | }) 283 | expect(tinyexec.exec).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { 284 | cwd: '/', 285 | }) 286 | }) 287 | 288 | test('with --verbose calls onExamineFile', async () => { 289 | const onExamineFile = jest.fn() 290 | mockGitFs() 291 | await prettyQuick('root', { since: 'banana', verbose: true, onExamineFile }) 292 | expect(onExamineFile).toHaveBeenCalledWith('./foo.js') 293 | expect(onExamineFile).toHaveBeenCalledWith('./bar.md') 294 | }) 295 | 296 | test('without --verbose does NOT call onExamineFile', async () => { 297 | const onExamineFile = jest.fn() 298 | mockGitFs() 299 | await prettyQuick('root', { since: 'banana', onExamineFile }) 300 | expect(onExamineFile).not.toHaveBeenCalledWith('./foo.js') 301 | expect(onExamineFile).not.toHaveBeenCalledWith('./bar.md') 302 | }) 303 | 304 | test('ignore files matching patterns from the repositories root .prettierignore', async () => { 305 | const onWriteFile = jest.fn() 306 | mockGitFs('', { 307 | '/.prettierignore': '*.md', 308 | }) 309 | // eslint-disable-next-line sonarjs/no-duplicate-string 310 | await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }) 311 | expect(onWriteFile.mock.calls).toEqual([['./foo.js']]) 312 | }) 313 | 314 | test('ignore files matching patterns from the working directories .prettierignore', async () => { 315 | const onWriteFile = jest.fn() 316 | mockGitFs('', { 317 | '/sub-directory/.prettierignore': '*.md', 318 | }) 319 | await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }) 320 | expect(onWriteFile.mock.calls).toEqual([['./foo.js']]) 321 | }) 322 | 323 | test('with --ignore-path to ignore files matching patterns from the repositories root .ignorePath', async () => { 324 | const onWriteFile = jest.fn() 325 | mockGitFs('', { 326 | '/.ignorePath': '*.md', 327 | }) 328 | await prettyQuick('/sub-directory/', { 329 | since: 'banana', 330 | onWriteFile, 331 | ignorePath: '/.ignorePath', 332 | }) 333 | expect(onWriteFile.mock.calls).toEqual([['./foo.js']]) 334 | }) 335 | 336 | test('with --ignore-path to ignore files matching patterns from the working directories .ignorePath', async () => { 337 | const onWriteFile = jest.fn() 338 | mockGitFs('', { 339 | '/sub-directory/.ignorePath': '*.md', 340 | }) 341 | await prettyQuick('/sub-directory/', { 342 | since: 'banana', 343 | onWriteFile, 344 | ignorePath: '/.ignorePath', 345 | }) 346 | expect(onWriteFile.mock.calls).toEqual([['./foo.js']]) 347 | }) 348 | }) 349 | -------------------------------------------------------------------------------- /test/scm-hg.spec.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | import mock from 'mock-fs' 4 | import type FileSystem from 'mock-fs/lib/filesystem' 5 | import * as tinyexec from 'tinyexec' 6 | 7 | import prettyQuick from 'pretty-quick' 8 | 9 | jest.mock('tinyexec') 10 | 11 | const mockHgFs = (additionalFiles?: FileSystem.DirectoryItems) => { 12 | mock({ 13 | '/.hg': {}, 14 | '/foo.js': 'foo()', 15 | '/bar.md': '# foo', 16 | ...additionalFiles, 17 | }) 18 | 19 | const execSpy = jest.spyOn(tinyexec, 'exec') as jest.Mock 20 | execSpy.mockImplementation((command: string, args: string[]) => { 21 | if (command !== 'hg') { 22 | throw new Error(`unexpected command: ${command}`) 23 | } 24 | switch (args[0]) { 25 | case 'status': { 26 | return { stdout: './foo.js\n' + './bar.md\n' } 27 | } 28 | case 'diff': { 29 | return { stdout: './foo.js\n' + './bar.md\n' } 30 | } 31 | case 'add': { 32 | return { stdout: '' } 33 | } 34 | case 'log': { 35 | return { stdout: '' } 36 | } 37 | default: { 38 | throw new Error(`unexpected arg0: ${args[0]}`) 39 | } 40 | } 41 | }) 42 | } 43 | 44 | afterEach(() => { 45 | mock.restore() 46 | jest.clearAllMocks() 47 | }) 48 | 49 | describe('with hg', () => { 50 | test('calls `hg debugancestor`', async () => { 51 | mock({ 52 | '/.hg': {}, 53 | }) 54 | await prettyQuick('root') 55 | expect(tinyexec.exec).toHaveBeenCalledWith( 56 | 'hg', 57 | ['debugancestor', 'tip', 'default'], 58 | { 59 | nodeOptions: { cwd: '/' }, 60 | }, 61 | ) 62 | }) 63 | 64 | test('calls `hg debugancestor` with root hg directory', async () => { 65 | mock({ 66 | '/.hg': {}, 67 | '/other-dir': {}, 68 | }) 69 | await prettyQuick('/other-dir') 70 | expect(tinyexec.exec).toHaveBeenCalledWith( 71 | 'hg', 72 | ['debugancestor', 'tip', 'default'], 73 | { 74 | nodeOptions: { cwd: '/' }, 75 | }, 76 | ) 77 | }) 78 | 79 | test('calls `hg status` with revision', async () => { 80 | mock({ 81 | '/.hg': {}, 82 | }) 83 | await prettyQuick('root', { since: 'banana' }) 84 | expect(tinyexec.exec).toHaveBeenCalledWith( 85 | 'hg', 86 | ['status', '-n', '-a', '-m', '--rev', 'banana'], 87 | { nodeOptions: { cwd: '/' } }, 88 | ) 89 | }) 90 | 91 | test('calls onFoundSinceRevision with return value from `hg debugancestor`', async () => { 92 | const onFoundSinceRevision = jest.fn() 93 | 94 | mock({ 95 | '/.hg': {}, 96 | }) 97 | 98 | const execSpy = jest.spyOn(tinyexec, 'exec') as jest.Mock 99 | execSpy.mockImplementation(() => ({ stdout: 'banana' })) 100 | 101 | await prettyQuick('root', { onFoundSinceRevision }) 102 | 103 | expect(onFoundSinceRevision).toHaveBeenCalledWith('hg', 'banana') 104 | }) 105 | 106 | test('calls onFoundChangedFiles with changed files', async () => { 107 | const onFoundChangedFiles = jest.fn() 108 | mockHgFs() 109 | await prettyQuick('root', { since: 'banana', onFoundChangedFiles }) 110 | expect(onFoundChangedFiles).toHaveBeenCalledWith(['./foo.js', './bar.md']) 111 | }) 112 | 113 | test('calls onWriteFile with changed files', async () => { 114 | const onWriteFile = jest.fn() 115 | mockHgFs() 116 | await prettyQuick('root', { since: 'banana', onWriteFile }) 117 | expect(onWriteFile).toHaveBeenCalledWith('./foo.js') 118 | expect(onWriteFile).toHaveBeenCalledWith('./bar.md') 119 | }) 120 | 121 | test('calls onWriteFile with changed files for the given pattern', async () => { 122 | const onWriteFile = jest.fn() 123 | mockHgFs() 124 | await prettyQuick('root', { pattern: '*.md', since: 'banana', onWriteFile }) 125 | expect(onWriteFile.mock.calls).toEqual([['./bar.md']]) 126 | }) 127 | 128 | test('calls onWriteFile with changed files for the given globstar pattern', async () => { 129 | const onWriteFile = jest.fn() 130 | mockHgFs() 131 | await prettyQuick('root', { 132 | pattern: '**/*.md', 133 | since: 'banana', 134 | onWriteFile, 135 | }) 136 | expect(onWriteFile.mock.calls).toEqual([['./bar.md']]) 137 | }) 138 | 139 | test('calls onWriteFile with changed files for the given extglob pattern', async () => { 140 | const onWriteFile = jest.fn() 141 | mockHgFs() 142 | await prettyQuick('root', { 143 | pattern: '*.*(md|foo|bar)', 144 | since: 'banana', 145 | onWriteFile, 146 | }) 147 | expect(onWriteFile.mock.calls).toEqual([['./bar.md']]) 148 | }) 149 | 150 | test('writes formatted files to disk', async () => { 151 | const onWriteFile = jest.fn() 152 | mockHgFs() 153 | await prettyQuick('root', { since: 'banana', onWriteFile }) 154 | expect(fs.readFileSync('/foo.js', 'utf8')).toEqual('formatted:foo()') 155 | expect(fs.readFileSync('/bar.md', 'utf8')).toEqual('formatted:# foo') 156 | }) 157 | 158 | test('succeeds if a file was changed and bail is not set', async () => { 159 | mockHgFs() 160 | const result = await prettyQuick('root', { since: 'banana' }) 161 | expect(result).toEqual({ errors: [], success: true }) 162 | }) 163 | 164 | test('fails if a file was changed and bail is set to true', async () => { 165 | mockHgFs() 166 | 167 | const result = await prettyQuick('root', { since: 'banana', bail: true }) 168 | 169 | expect(result).toEqual({ errors: ['BAIL_ON_WRITE'], success: false }) 170 | }) 171 | 172 | test('calls onWriteFile with changed files for an array of globstar patterns', async () => { 173 | const onWriteFile = jest.fn() 174 | mockHgFs() 175 | await prettyQuick('root', { 176 | pattern: ['**/*.foo', '**/*.md', '**/*.bar'], 177 | since: 'banana', 178 | onWriteFile, 179 | }) 180 | expect(onWriteFile.mock.calls).toEqual([['./bar.md']]) 181 | }) 182 | 183 | test('without --staged does NOT stage changed files', async () => { 184 | mockHgFs() 185 | await prettyQuick('root', { since: 'banana' }) 186 | expect(tinyexec.exec).not.toHaveBeenCalledWith('hg', ['add', './foo.js'], { 187 | nodeOptions: { cwd: '/' }, 188 | }) 189 | expect(tinyexec.exec).not.toHaveBeenCalledWith('hg', ['add', './bar.md'], { 190 | nodeOptions: { cwd: '/' }, 191 | }) 192 | }) 193 | 194 | test('with --verbose calls onExamineFile', async () => { 195 | const onExamineFile = jest.fn() 196 | mockHgFs() 197 | await prettyQuick('root', { since: 'banana', verbose: true, onExamineFile }) 198 | expect(onExamineFile).toHaveBeenCalledWith('./foo.js') 199 | expect(onExamineFile).toHaveBeenCalledWith('./bar.md') 200 | }) 201 | 202 | test('without --verbose does NOT call onExamineFile', async () => { 203 | const onExamineFile = jest.fn() 204 | mockHgFs() 205 | await prettyQuick('root', { since: 'banana', onExamineFile }) 206 | expect(onExamineFile).not.toHaveBeenCalledWith('./foo.js') 207 | expect(onExamineFile).not.toHaveBeenCalledWith('./bar.md') 208 | }) 209 | 210 | test('ignore files matching patterns from the repositories root .prettierignore', async () => { 211 | const onWriteFile = jest.fn() 212 | mockHgFs({ 213 | '/.prettierignore': '*.md', 214 | }) 215 | // eslint-disable-next-line sonarjs/no-duplicate-string 216 | await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }) 217 | expect(onWriteFile.mock.calls).toEqual([['./foo.js']]) 218 | }) 219 | 220 | test('ignore files matching patterns from the working directories .prettierignore', async () => { 221 | const onWriteFile = jest.fn() 222 | mockHgFs({ 223 | '/sub-directory/.prettierignore': '*.md', 224 | }) 225 | await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }) 226 | expect(onWriteFile.mock.calls).toEqual([['./foo.js']]) 227 | }) 228 | 229 | test('with --ignore-path to ignore files matching patterns from the repositories root .ignorePath', async () => { 230 | const onWriteFile = jest.fn() 231 | mockHgFs({ 232 | '/.ignorePath': '*.md', 233 | }) 234 | await prettyQuick('/sub-directory/', { 235 | since: 'banana', 236 | onWriteFile, 237 | ignorePath: '/.ignorePath', 238 | }) 239 | expect(onWriteFile.mock.calls).toEqual([['./foo.js']]) 240 | }) 241 | 242 | test('with --ignore-path to ignore files matching patterns from the working directories .ignorePath', async () => { 243 | const onWriteFile = jest.fn() 244 | mockHgFs({ 245 | '/.ignorePath': '*.md', 246 | }) 247 | await prettyQuick('/sub-directory/', { 248 | since: 'banana', 249 | onWriteFile, 250 | ignorePath: '/.ignorePath', 251 | }) 252 | expect(onWriteFile.mock.calls).toEqual([['./foo.js']]) 253 | }) 254 | }) 255 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | // used by @pkgr/rollup and ESLint 3 | "extends": "./tsconfig.lib", 4 | "compilerOptions": { 5 | "paths": { 6 | "pretty-quick": ["./src"], 7 | "pretty-quick/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "ts-node": { 7 | "transpileOnly": true 8 | }, 9 | "references": [ 10 | { 11 | "path": "./src" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | // used by all files but do not includes paths 3 | "extends": "@1stg/tsconfig/node16" 4 | } 5 | --------------------------------------------------------------------------------