├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── release.yml │ └── snapshot-release.yml ├── .gitignore ├── .husky └── pre-commit ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── fancy.ts └── index.ts ├── test ├── fancy.ts ├── fixtures.ts └── fixtures │ ├── index.tpl │ └── tsconfig.json └── tsconfig.json /.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@1.6.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | jest 2 | lib 3 | test/fixtures -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@aiou'], 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: JiangWeixian # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Screenshots** 11 | If applicable, add screenshots to help explain your problem. 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **To Reproduce** 20 | Steps to reproduce the behavior: 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | **Version (please complete the following information):** 27 | - Version [e.g. 22] 28 | - OS [e.g macos] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | - Mini reproduction 33 | 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe what problem does this feature solve?** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe what does the proposed API look like?** 14 | Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use Markdown to format your code blocks. 15 | 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | **What kind of change does this PR introduce?** (check at least one) 5 | 6 | - [ ] Bugfix 7 | - [ ] Feature 8 | - [ ] Code style update 9 | - [ ] Refactor 10 | - [ ] Build-related changes 11 | - [ ] Other, please describe: 12 | 13 | **Does this PR introduce a breaking change?** (check one) 14 | 15 | - [ ] Yes 16 | - [ ] No 17 | 18 | If yes, please describe the impact and migration path for existing applications: 19 | 20 | **The PR fulfills these requirements:** 21 | 22 | - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number) 23 | - [ ] All tests are passing 24 | - [ ] New/updated tests are included 25 | 26 | If adding a **new feature**, the PR's description includes: 27 | - [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it) 28 | 29 | **Other information:** 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [master,release] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | name: test 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x, 14.x, 16.x] 16 | 17 | steps: 18 | - name: checkout code repository 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: Install pnpm 27 | run: npm i pnpm@latest -g 28 | - name: Install 29 | run: | 30 | pnpm install --frozen-lockfile=false 31 | - name: Test 32 | run: | 33 | pnpm test -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | - 'releases/*' 8 | env: 9 | CI: true 10 | jobs: 11 | version: 12 | timeout-minutes: 15 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout code repository 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: setup node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 14 23 | - name: install pnpm 24 | run: npm i pnpm@latest -g 25 | - name: install dependencies 26 | run: pnpm install --frozen-lockfile=false 27 | - name: create and publish versions 28 | uses: changesets/action@master 29 | with: 30 | version: pnpm ci:version 31 | commit: "chore: update versions" 32 | title: "chore: update versions" 33 | publish: pnpm ci:publish 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/snapshot-release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - snapshot 6 | env: 7 | CI: true 8 | jobs: 9 | version: 10 | timeout-minutes: 15 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout code repository 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - name: setup node.js 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 14 21 | - name: install pnpm 22 | run: npm i pnpm@latest -g 23 | - name: install dependencies 24 | run: pnpm install --frozen-lockfile=false 25 | - name: create and publish versions 26 | uses: changesets/action@master 27 | with: 28 | version: pnpm ci:snapshot 29 | commit: "chore: update versions" 30 | title: "chore: update versions" 31 | publish: pnpm ci:prerelease 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # jest 30 | jest 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | .env.production 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | .parcel-cache 83 | 84 | # Next.js build output 85 | .next 86 | out 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Templates Common build 93 | lib 94 | es 95 | .lib 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # Serverless directories 107 | .serverless/ 108 | 109 | # FuseBox cache 110 | .fusebox/ 111 | 112 | # DynamoDB Local files 113 | .dynamodb/ 114 | 115 | # TernJS port file 116 | .tern-port 117 | 118 | # Stores VSCode versions used for testing VSCode extensions 119 | .vscode-test 120 | 121 | # yarn v2 122 | .yarn/cache 123 | .yarn/unplugged 124 | .yarn/build-state.yml 125 | .yarn/install-state.gz 126 | .pnp.* 127 | # General 128 | .DS_Store 129 | .AppleDouble 130 | .LSOverride 131 | 132 | # Icon must end with two \r 133 | Icon 134 | 135 | 136 | # Thumbnails 137 | ._* 138 | 139 | # Files that might appear in the root of a volume 140 | .DocumentRevisions-V100 141 | .fseventsd 142 | .Spotlight-V100 143 | .TemporaryItems 144 | .Trashes 145 | .VolumeIcon.icns 146 | .com.apple.timemachine.donotpresent 147 | 148 | # Directories potentially created on remote AFP share 149 | .AppleDB 150 | .AppleDesktop 151 | Network Trash Folder 152 | Temporary Items 153 | .apdisk 154 | 155 | # Fixtures 156 | test/fixtures/fake -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpx lint-staged 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # tsc-progress 2 | 3 | ## 1.0.4 4 | 5 | ### Patch Changes 6 | 7 | - da9ac76: fix watch mode stats 8 | 9 | ## 1.0.3 10 | 11 | ### Patch Changes 12 | 13 | - a860ebc: fix progress not working on child_process 14 | 15 | ## 1.0.2 16 | 17 | ### Patch Changes 18 | 19 | - 7bf789b: deprecated tslib 20 | 21 | ## 1.0.1 22 | 23 | ### Patch Changes 24 | 25 | - 11af950: update docs 26 | 27 | ## 1.0.0 28 | 29 | ### Major Changes 30 | 31 | - 3155550: mvp version, display progress bar on ttsc build 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 JW 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 | # tsc-progress 2 | *ttypescript and ts-patch build progressbar, inspired by [webpackbar](https://github.com/unjs/webpackbar)* 3 | 4 | [![npm](https://img.shields.io/npm/v/tsc-progress)](https://github.com/JiangWeixian/tsc-progress/tree/master) [![GitHub](https://img.shields.io/npm/l/tsc-progress)](https://github.com/JiangWeixian/tsc-progress/tree/master) 5 | 6 | ![image](https://user-images.githubusercontent.com/6839576/147484015-79fb0df1-eee4-438a-b14e-d4cf82b2f3fc.png) 7 | 8 | ## install 9 | 10 | ```console 11 | npm i tsc-progress 12 | ``` 13 | ## usage 14 | 15 | in `tsconfig.json` 16 | 17 | ```json 18 | { 19 | // ... 20 | "plugins": [ 21 | { 22 | "transform": "tsc-progress", 23 | "title": "TSC" 24 | } 25 | ] 26 | } 27 | ``` 28 | 29 | `options` 30 | 31 | - `title` - define progressbar title, default `TSC` 32 | - `color` - define progressbar color, default `green` 33 | 34 | # 35 |
36 | 37 | *built with ❤️ by 😼* 38 | 39 |
40 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: './jest/cache', 3 | collectCoverage: true, 4 | collectCoverageFrom: ['src/**/*'], 5 | coverageDirectory: './jest/coverage', 6 | preset: 'ts-jest', 7 | resetMocks: true, 8 | resetModules: true, 9 | restoreMocks: true, 10 | globals: { 11 | 'ts-jest': { 12 | diagnostics: false, 13 | }, 14 | }, 15 | moduleNameMapper: { 16 | '@/(.*)': '/src/$1', 17 | }, 18 | roots: ['/test'], 19 | moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'], 20 | testRegex: '/test/.+\\.test\\.tsx?$', 21 | verbose: false, 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsc-progress", 3 | "version": "1.0.4", 4 | "description": "display progress bar in tsc build", 5 | "keywords": [ 6 | "typescript", 7 | "build", 8 | "webpackbar", 9 | "progressbar" 10 | ], 11 | "license": "MIT", 12 | "homepage": "https://github.com/JiangWeixian/tsc-progress#readme", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/JiangWeixian/tsc-progress.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/JiangWeixian/tsc-progress/issues", 19 | "email": "jiangweixian1994@gmail.com" 20 | }, 21 | "author": { 22 | "name": "JW", 23 | "email": "jiangweixian1994@gmail.com", 24 | "url": "https://twitter.com/jiangweixian" 25 | }, 26 | "files": [ 27 | "lib" 28 | ], 29 | "main": "lib/index.js", 30 | "typings": "lib/index.d.ts", 31 | "scripts": { 32 | "build": "rimraf lib && ttsc", 33 | "pretest": "pnpm run build && esrua ./test/fixtures.ts main", 34 | "test": "pnpm run pretest && cd ./test/fixtures && ttsc", 35 | "watch:test": "pnpm run pretest && cd ./test/fixtures && ttsc -w", 36 | "ci:publish": "pnpm run build && pnpx changeset publish", 37 | "ci:version": "pnpx changeset version", 38 | "ci:snapshot": "pnpx changeset version --snapshot beta", 39 | "ci:prerelease": "pnpm run build && pnpx changeset publish --tag beta", 40 | "lint:fix": "eslint . --fix" 41 | }, 42 | "lint-staged": { 43 | "**/**/*.{js,ts,tsx,vue,json}": [ 44 | "eslint --fix" 45 | ] 46 | }, 47 | "peerDependencies": { 48 | "typescript": "*" 49 | }, 50 | "dependencies": { 51 | "ansi-escapes": "4.x", 52 | "figures": "3.x", 53 | "markdown-table": "^3.0.2", 54 | "picocolors": "^1.0.0", 55 | "pretty-time": "^1.1.0", 56 | "wrap-ansi": "7.x" 57 | }, 58 | "devDependencies": { 59 | "@aiou/eslint-config": "0.3.2", 60 | "@changesets/cli": "^2.16.0", 61 | "@types/cli-progress": "^3.9.2", 62 | "@types/fs-extra": "^9.0.13", 63 | "@types/jest": "26.0.23", 64 | "@types/node": "^17.0.4", 65 | "@types/pretty-time": "^1.1.2", 66 | "@types/wrap-ansi": "^8.0.1", 67 | "cz-emoji": "^1.3.1", 68 | "eslint": "^7.30.0", 69 | "esrua": "^0.1.0", 70 | "fs-extra": "^10.0.0", 71 | "husky": "^7.0.0", 72 | "jest": "27.0.6", 73 | "lint-staged": "^11.0.1", 74 | "np": "7.5.0", 75 | "npm-watch": "0.10.0", 76 | "prettier": "2.3.2", 77 | "pretty-quick": "3.1.1", 78 | "rimraf": "3.0.2", 79 | "tempy": "1.x", 80 | "ts-jest": "27.0.3", 81 | "ts-node": "10.0.0", 82 | "tslib": "2.3.0", 83 | "ttypescript": "^1.5.12", 84 | "typescript": "^4.3.5", 85 | "typescript-transform-extensions": "^1.0.1", 86 | "typescript-transform-paths": "^3.0.2" 87 | }, 88 | "config": { 89 | "commitizen": { 90 | "path": "cz-emoji" 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/fancy.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // progress bar code from refs: https://github.com/unjs/webpackbar 3 | import pc from 'picocolors' 4 | import ansiEscapes from 'ansi-escapes' 5 | import wrapAnsi from 'wrap-ansi' 6 | import figures from 'figures' 7 | 8 | const { bullet, tick, cross, radioOff, pointerSmall } = figures 9 | const BAR_LENGTH = 25 10 | const BLOCK_CHAR = '█' 11 | const BLOCK_CHAR2 = '█' 12 | const NEXT = ` ${pc.blue(pointerSmall)} ` 13 | const BULLET = bullet 14 | const TICK = tick 15 | const CROSS = cross 16 | const CIRCLE_OPEN = radioOff 17 | 18 | function range(len) { 19 | const arr = [] 20 | for (let i = 0; i < len; i++) { 21 | arr.push(i) 22 | } 23 | return arr 24 | } 25 | 26 | export const colorize = (color) => { 27 | if (color[0] === '#') { 28 | return pc.hex(color) 29 | } 30 | 31 | return pc[color] || pc.keyword(color) 32 | } 33 | 34 | export const renderBar = (progress, color) => { 35 | const w = progress * (BAR_LENGTH / 100) 36 | const bg = pc.white(BLOCK_CHAR) 37 | const fg = colorize(color)(BLOCK_CHAR2) 38 | 39 | return range(BAR_LENGTH) 40 | .map((i) => (i < w ? fg : bg)) 41 | .join('') 42 | } 43 | 44 | export function ellipsis(str, n) { 45 | if (str.length <= n - 3) { 46 | return str 47 | } 48 | return `${str.substr(0, n - 1)}...` 49 | } 50 | 51 | export function ellipsisLeft(str, n) { 52 | if (str.length <= n - 3) { 53 | return str 54 | } 55 | return `...${str.substr(str.length - n - 1)}` 56 | } 57 | 58 | export const formatRequest = (request) => { 59 | const loaders = request.loaders.join(NEXT) 60 | 61 | if (!loaders.length) { 62 | return request.file || '' 63 | } 64 | 65 | return `${loaders}${NEXT}${request.file}` 66 | } 67 | 68 | const originalWrite = Symbol('tscWrite') 69 | class LogUpdate { 70 | private prevLineCount: any 71 | private listening: any 72 | private extraLines: any 73 | private _streams: any 74 | 75 | constructor() { 76 | this.prevLineCount = 0 77 | this.listening = false 78 | this.extraLines = '' 79 | this._onData = this._onData.bind(this) 80 | this._streams = [process.stdout, process.stderr] 81 | } 82 | 83 | render(lines) { 84 | this.listen() 85 | 86 | const wrappedLines = wrapAnsi(lines, this.columns, { 87 | trim: false, 88 | hard: true, 89 | wordWrap: false, 90 | }) 91 | 92 | const data = `${ansiEscapes.eraseLines(this.prevLineCount) + wrappedLines}\n${this.extraLines}` 93 | 94 | this.write(data) 95 | 96 | const _lines = data.split('\n') 97 | this.prevLineCount = _lines.length 98 | 99 | // Count wrapped line too 100 | // https://github.com/unjs/webpackbar/pull/90 101 | // TODO: Count length with regards of control chars 102 | // this.prevLineCount += _lines.reduce((s, l) => s + Math.floor(l.length / this.columns), 0) 103 | } 104 | 105 | get columns() { 106 | return (process.stderr.columns || 80) - 2 107 | } 108 | 109 | write(data) { 110 | const stream = process.stderr 111 | if (stream.write[originalWrite]) { 112 | stream.write[originalWrite].call(stream, data, 'utf-8') 113 | } else { 114 | stream.write(data, 'utf-8') 115 | } 116 | } 117 | 118 | clear() { 119 | this.done() 120 | this.write(ansiEscapes.eraseLines(this.prevLineCount)) 121 | } 122 | 123 | done() { 124 | this.stopListen() 125 | 126 | this.prevLineCount = 0 127 | this.extraLines = '' 128 | } 129 | 130 | _onData(data) { 131 | const str = String(data) 132 | const lines = str.split('\n').length - 1 133 | if (lines > 0) { 134 | this.prevLineCount += lines 135 | this.extraLines += data 136 | } 137 | } 138 | 139 | listen() { 140 | // Prevent listening more than once 141 | if (this.listening) { 142 | return 143 | } 144 | 145 | // Spy on all streams 146 | for (const stream of this._streams) { 147 | // Prevent overriding more than once 148 | if (stream.write[originalWrite]) { 149 | continue 150 | } 151 | 152 | // Create a wrapper fn 153 | const write = (data, ...args) => { 154 | if (!stream.write[originalWrite]) { 155 | return stream.write(data, ...args) 156 | } 157 | this._onData(data) 158 | return stream.write[originalWrite].call(stream, data, ...args) 159 | } 160 | 161 | // Backup original write fn 162 | write[originalWrite] = stream.write 163 | 164 | // Override write fn 165 | stream.write = write 166 | } 167 | 168 | this.listening = true 169 | } 170 | 171 | stopListen() { 172 | // Restore original write fns 173 | for (const stream of this._streams) { 174 | if (stream.write[originalWrite]) { 175 | stream.write = stream.write[originalWrite] 176 | } 177 | } 178 | 179 | this.listening = false 180 | } 181 | } 182 | 183 | const logUpdate = new LogUpdate() 184 | 185 | interface State { 186 | start: [number, number] | null 187 | progress: number 188 | done: boolean 189 | message: string 190 | details: string[] 191 | request: null | { 192 | file: null | string 193 | loaders: string[] 194 | } 195 | hasErrors: boolean 196 | color: string 197 | name: string 198 | } 199 | 200 | export default class FancyReporter { 201 | statesArray: State[] = [] 202 | hasErrors = false 203 | 204 | updateStatesArray(statesArray: State[]) { 205 | this.statesArray = statesArray 206 | } 207 | 208 | allDone() { 209 | logUpdate.done() 210 | } 211 | 212 | done() { 213 | this._renderStates(this.statesArray) 214 | 215 | if (this.hasErrors) { 216 | logUpdate.done() 217 | } 218 | } 219 | 220 | progress() { 221 | this._renderStates(this.statesArray) 222 | } 223 | 224 | _renderStates(statesArray: State[]) { 225 | const renderedStates = statesArray.map((c) => this._renderState(c)).join('\n\n') 226 | 227 | logUpdate.render(`\n${renderedStates}\n`) 228 | } 229 | 230 | _renderState(state: State) { 231 | const color = colorize(state.color) 232 | 233 | let line1 234 | let line2 235 | 236 | if (state.progress >= 0 && state.progress < 100) { 237 | // Running 238 | line1 = [ 239 | color(BULLET), 240 | color(state.name), 241 | renderBar(state.progress, state.color), 242 | state.message, 243 | `(${state.progress || 0}%)`, 244 | pc.gray(state.details[0] || ''), 245 | pc.gray(state.details[1] || ''), 246 | ].join(' ') 247 | 248 | line2 = state.request 249 | ? ` ${pc.gray(ellipsisLeft(formatRequest(state.request), logUpdate.columns))}` 250 | : '' 251 | } else { 252 | let icon = ' ' 253 | 254 | if (state.hasErrors) { 255 | icon = CROSS 256 | } else if (state.progress === 100) { 257 | icon = TICK 258 | } else if (state.progress === -1) { 259 | icon = CIRCLE_OPEN 260 | } 261 | 262 | line1 = color(`${icon} ${state.name}`) 263 | line2 = pc.gray(` ${state.message}`) 264 | } 265 | 266 | return `${line1}\n${line2}` 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript' 2 | import prettyTime from 'pretty-time' 3 | 4 | import FancyReporter from './fancy' 5 | 6 | const reporter = new FancyReporter() 7 | 8 | type Options = { 9 | title: string 10 | color: string 11 | } 12 | 13 | const progress = { 14 | total: 0, 15 | loaded: 0, 16 | start: process.hrtime(), 17 | } 18 | 19 | export default function tscProgress( 20 | program: ts.Program, 21 | options: Options, 22 | ): ts.TransformerFactory { 23 | const total = program.getRootFileNames().filter((filepath) => !filepath.endsWith('.d.ts')).length 24 | 25 | progress.total = total 26 | 27 | return () => { 28 | return (sourceFile: ts.SourceFile) => { 29 | progress.loaded += 1 30 | let percent = 99.9 31 | if (progress.total > 0) { 32 | percent = Math.round((100 * progress.loaded) / progress.total) 33 | } 34 | reporter.updateStatesArray([ 35 | { 36 | name: options.title || 'TSC', 37 | progress: percent, 38 | color: options.color || 'green', 39 | details: [sourceFile.fileName], 40 | message: 41 | // in watch mode, loaded more than total 42 | percent < 100 43 | ? 'building' 44 | : `Compiled successfully in ${prettyTime(process.hrtime(progress.start))}`, 45 | hasErrors: false, 46 | done: false, 47 | start: null, 48 | request: null, 49 | }, 50 | ]) 51 | reporter.progress() 52 | return sourceFile 53 | } 54 | } 55 | } 56 | 57 | process.on('exit', () => { 58 | reporter.done() 59 | }) 60 | -------------------------------------------------------------------------------- /test/fancy.ts: -------------------------------------------------------------------------------- 1 | import FancyReporter from '../src/fancy' 2 | 3 | const reporter = new FancyReporter() 4 | 5 | export const progress = () => { 6 | let progress = 0 7 | setInterval(() => { 8 | if (progress >= 100) { 9 | return 10 | } 11 | progress += 10 12 | reporter.updateStatesArray([ 13 | { 14 | progress, 15 | details: [], 16 | color: 'green', 17 | name: 'TSC', 18 | message: progress === 100 ? 'Success' : 'Building', 19 | hasErrors: false, 20 | done: false, 21 | start: null, 22 | request: null, 23 | }, 24 | ]) 25 | reporter.progress() 26 | if (progress === 100) { 27 | reporter.done() 28 | } 29 | }, 1000) 30 | } 31 | -------------------------------------------------------------------------------- /test/fixtures.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import path from 'path' 3 | import fs from 'fs-extra' 4 | 5 | const content = fs.readFileSync(path.join(__dirname, './fixtures/index.tpl')).toString() 6 | 7 | export const main = async () => { 8 | const dir = path.join(__dirname, 'fixtures/fake') 9 | fs.ensureDirSync(dir) 10 | for (let i = 0; i < 1000; i++) { 11 | const filename = i === 0 ? 'index.ts' : `index${i}.ts` 12 | fs.outputFileSync(path.join(dir, filename), content) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/index.tpl: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { isAbsolute, resolve, sep } from 'path'; 3 | import { Job } from './node-file-trace'; 4 | 5 | // node resolver 6 | // custom implementation to emit only needed package.json files for resolver 7 | // (package.json files are emitted as they are hit) 8 | export default function resolveDependency (specifier: string, parent: string, job: Job, cjsResolve = true) { 9 | let resolved: string | string[]; 10 | if (isAbsolute(specifier) || specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../')) { 11 | const trailingSlash = specifier.endsWith('/'); 12 | resolved = resolvePath(resolve(parent, '..', specifier) + (trailingSlash ? '/' : ''), parent, job); 13 | } 14 | else if (specifier[0] === '#') { 15 | resolved = packageImportsResolve(specifier, parent, job, cjsResolve); 16 | } 17 | else { 18 | resolved = resolvePackage(specifier, parent, job, cjsResolve); 19 | } 20 | 21 | if (Array.isArray(resolved)) { 22 | return resolved.map(resolved => job.realpath(resolved, parent)); 23 | } else if (resolved.startsWith('node:')) { 24 | return resolved; 25 | } else { 26 | return job.realpath(resolved, parent); 27 | } 28 | }; 29 | 30 | function resolvePath (path: string, parent: string, job: Job): string { 31 | const result = resolveFile(path, parent, job) || resolveDir(path, parent, job); 32 | if (!result) { 33 | throw new NotFoundError(path, parent); 34 | } 35 | return result; 36 | } 37 | 38 | function resolveFile (path: string, parent: string, job: Job): string | undefined { 39 | if (path.endsWith('/')) return undefined; 40 | path = job.realpath(path, parent); 41 | if (job.isFile(path)) return path; 42 | if (job.ts && path.startsWith(job.base) && path.substr(job.base.length).indexOf(sep + 'node_modules' + sep) === -1 && job.isFile(path + '.ts')) return path + '.ts'; 43 | if (job.ts && path.startsWith(job.base) && path.substr(job.base.length).indexOf(sep + 'node_modules' + sep) === -1 && job.isFile(path + '.tsx')) return path + '.tsx'; 44 | if (job.isFile(path + '.js')) return path + '.js'; 45 | if (job.isFile(path + '.json')) return path + '.json'; 46 | if (job.isFile(path + '.node')) return path + '.node'; 47 | return undefined; 48 | } 49 | 50 | function resolveDir (path: string, parent: string, job: Job) { 51 | if (path.endsWith('/')) path = path.slice(0, -1); 52 | if (!job.isDir(path)) return; 53 | const pkgCfg = getPkgCfg(path, job); 54 | if (pkgCfg && typeof pkgCfg.main === 'string') { 55 | const resolved = resolveFile(resolve(path, pkgCfg.main), parent, job) || resolveFile(resolve(path, pkgCfg.main, 'index'), parent, job); 56 | if (resolved) { 57 | job.emitFile(path + sep + 'package.json', 'resolve', parent); 58 | return resolved; 59 | } 60 | } 61 | return resolveFile(resolve(path, 'index'), parent, job); 62 | } 63 | 64 | class NotFoundError extends Error { 65 | public code: string; 66 | constructor(specifier: string, parent: string) { 67 | super("Cannot find module '" + specifier + "' loaded from " + parent); 68 | this.code = 'MODULE_NOT_FOUND'; 69 | } 70 | } 71 | 72 | const nodeBuiltins = new Set([...require("repl")._builtinLibs, "constants", "module", "timers", "console", "_stream_writable", "_stream_readable", "_stream_duplex", "process", "sys"]); 73 | 74 | function getPkgName (name: string) { 75 | const segments = name.split('/'); 76 | if (name[0] === '@' && segments.length > 1) 77 | return segments.length > 1 ? segments.slice(0, 2).join('/') : null; 78 | return segments.length ? segments[0] : null; 79 | } 80 | 81 | type PackageTarget = string | PackageTarget[] | { [key: string]: PackageTarget } | null; 82 | 83 | interface PkgCfg { 84 | name: string | undefined; 85 | main: string | undefined; 86 | exports: PackageTarget; 87 | imports: { [key: string]: PackageTarget }; 88 | } 89 | 90 | function getPkgCfg (pkgPath: string, job: Job): PkgCfg | undefined { 91 | const pjsonSource = job.readFile(pkgPath + sep + 'package.json'); 92 | if (pjsonSource) { 93 | try { 94 | return JSON.parse(pjsonSource.toString()); 95 | } 96 | catch (e) {} 97 | } 98 | return undefined; 99 | } 100 | 101 | function getExportsTarget(exports: PackageTarget, conditions: string[], cjsResolve: boolean): string | null | undefined { 102 | if (typeof exports === 'string') { 103 | return exports; 104 | } 105 | else if (exports === null) { 106 | return exports; 107 | } 108 | else if (Array.isArray(exports)) { 109 | for (const item of exports) { 110 | const target = getExportsTarget(item, conditions, cjsResolve); 111 | if (target === null || typeof target === 'string' && target.startsWith('./')) 112 | return target; 113 | } 114 | } 115 | else if (typeof exports === 'object') { 116 | for (const condition of Object.keys(exports)) { 117 | if (condition === 'default' || 118 | condition === 'require' && cjsResolve || 119 | condition === 'import' && !cjsResolve || 120 | conditions.includes(condition)) { 121 | const target = getExportsTarget(exports[condition], conditions, cjsResolve); 122 | if (target !== undefined) 123 | return target; 124 | } 125 | } 126 | } 127 | 128 | return undefined; 129 | } 130 | 131 | function resolveExportsImports (pkgPath: string, obj: PackageTarget, subpath: string, job: Job, isImports: boolean, cjsResolve: boolean): string | undefined { 132 | let matchObj: { [key: string]: PackageTarget }; 133 | if (isImports) { 134 | if (!(typeof obj === 'object' && !Array.isArray(obj) && obj !== null)) 135 | return undefined; 136 | matchObj = obj; 137 | } else if (typeof obj === 'string' || Array.isArray(obj) || obj === null || 138 | typeof obj === 'object' && Object.keys(obj).length && Object.keys(obj)[0][0] !== '.') { 139 | matchObj = { '.' : obj }; 140 | } else { 141 | matchObj = obj; 142 | } 143 | 144 | if (subpath in matchObj) { 145 | const target = getExportsTarget(matchObj[subpath], job.conditions, cjsResolve); 146 | if (typeof target === 'string' && target.startsWith('./')) 147 | return pkgPath + target.slice(1); 148 | } 149 | for (const match of Object.keys(matchObj).sort((a, b) => b.length - a.length)) { 150 | if (match.endsWith('*') && subpath.startsWith(match.slice(0, -1))) { 151 | const target = getExportsTarget(matchObj[match], job.conditions, cjsResolve); 152 | if (typeof target === 'string' && target.startsWith('./')) 153 | return pkgPath + target.slice(1).replace(/\*/g, subpath.slice(match.length - 1)); 154 | } 155 | if (!match.endsWith('/')) 156 | continue; 157 | if (subpath.startsWith(match)) { 158 | const target = getExportsTarget(matchObj[match], job.conditions, cjsResolve); 159 | if (typeof target === 'string' && target.endsWith('/') && target.startsWith('./')) 160 | return pkgPath + target.slice(1) + subpath.slice(match.length); 161 | } 162 | } 163 | return undefined; 164 | } 165 | 166 | function packageImportsResolve (name: string, parent: string, job: Job, cjsResolve: boolean): string { 167 | if (name !== '#' && !name.startsWith('#/') && job.conditions) { 168 | const pjsonBoundary = job.getPjsonBoundary(parent); 169 | if (pjsonBoundary) { 170 | const pkgCfg = getPkgCfg(pjsonBoundary, job); 171 | const { imports: pkgImports } = pkgCfg || {}; 172 | if (pkgCfg && pkgImports !== null && pkgImports !== undefined) { 173 | let importsResolved = resolveExportsImports(pjsonBoundary, pkgImports, name, job, true, cjsResolve); 174 | if (importsResolved) { 175 | if (cjsResolve) 176 | importsResolved = resolveFile(importsResolved, parent, job) || resolveDir(importsResolved, parent, job); 177 | else if (!job.isFile(importsResolved)) 178 | throw new NotFoundError(importsResolved, parent); 179 | if (importsResolved) { 180 | job.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', parent); 181 | return importsResolved; 182 | } 183 | } 184 | } 185 | } 186 | } 187 | throw new NotFoundError(name, parent); 188 | } 189 | 190 | function resolvePackage (name: string, parent: string, job: Job, cjsResolve: boolean): string | string [] { 191 | let packageParent = parent; 192 | if (nodeBuiltins.has(name)) return 'node:' + name; 193 | 194 | const pkgName = getPkgName(name) || ''; 195 | 196 | // package own name resolution 197 | let selfResolved: string | undefined; 198 | if (job.conditions) { 199 | const pjsonBoundary = job.getPjsonBoundary(parent); 200 | if (pjsonBoundary) { 201 | const pkgCfg = getPkgCfg(pjsonBoundary, job); 202 | const { exports: pkgExports } = pkgCfg || {}; 203 | if (pkgCfg && pkgCfg.name && pkgExports !== null && pkgExports !== undefined) { 204 | selfResolved = resolveExportsImports(pjsonBoundary, pkgExports, '.' + name.slice(pkgName.length), job, false, cjsResolve); 205 | if (selfResolved) { 206 | if (cjsResolve) 207 | selfResolved = resolveFile(selfResolved, parent, job) || resolveDir(selfResolved, parent, job); 208 | else if (!job.isFile(selfResolved)) 209 | throw new NotFoundError(selfResolved, parent); 210 | } 211 | if (selfResolved) 212 | job.emitFile(pjsonBoundary + sep + 'package.json', 'resolve', parent); 213 | } 214 | } 215 | } 216 | 217 | let separatorIndex: number; 218 | const rootSeparatorIndex = packageParent.indexOf(sep); 219 | while ((separatorIndex = packageParent.lastIndexOf(sep)) > rootSeparatorIndex) { 220 | packageParent = packageParent.substr(0, separatorIndex); 221 | const nodeModulesDir = packageParent + sep + 'node_modules'; 222 | const stat = job.stat(nodeModulesDir); 223 | if (!stat || !stat.isDirectory()) continue; 224 | const pkgCfg = getPkgCfg(nodeModulesDir + sep + pkgName, job); 225 | const { exports: pkgExports } = pkgCfg || {}; 226 | if (job.conditions && pkgExports !== undefined && pkgExports !== null && !selfResolved) { 227 | let legacyResolved; 228 | if (!job.exportsOnly) 229 | legacyResolved = resolveFile(nodeModulesDir + sep + name, parent, job) || resolveDir(nodeModulesDir + sep + name, parent, job); 230 | let resolved = resolveExportsImports(nodeModulesDir + sep + pkgName, pkgExports, '.' + name.slice(pkgName.length), job, false, cjsResolve); 231 | if (resolved) { 232 | if (cjsResolve) 233 | resolved = resolveFile(resolved, parent, job) || resolveDir(resolved, parent, job); 234 | else if (!job.isFile(resolved)) 235 | throw new NotFoundError(resolved, parent); 236 | } 237 | if (resolved) { 238 | job.emitFile(nodeModulesDir + sep + pkgName + sep + 'package.json', 'resolve', parent); 239 | if (legacyResolved && legacyResolved !== resolved) 240 | return [resolved, legacyResolved]; 241 | return resolved; 242 | } 243 | if (legacyResolved) 244 | return legacyResolved; 245 | } 246 | else { 247 | const resolved = resolveFile(nodeModulesDir + sep + name, parent, job) || resolveDir(nodeModulesDir + sep + name, parent, job); 248 | if (resolved) { 249 | if (selfResolved && selfResolved !== resolved) 250 | return [resolved, selfResolved]; 251 | return resolved; 252 | } 253 | } 254 | } 255 | if (selfResolved) return selfResolved; 256 | if (Object.hasOwnProperty.call(job.paths, name)) { 257 | return job.paths[name]; 258 | } 259 | for (const path of Object.keys(job.paths)) { 260 | if (path.endsWith('/') && name.startsWith(path)) { 261 | const pathTarget = job.paths[path] + name.slice(path.length); 262 | const resolved = resolveFile(pathTarget, parent, job) || resolveDir(pathTarget, parent, job); 263 | if (!resolved) { 264 | throw new NotFoundError(name, parent); 265 | } 266 | return resolved; 267 | } 268 | } 269 | throw new NotFoundError(name, parent); 270 | } -------------------------------------------------------------------------------- /test/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "importHelpers": false, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "sourceMap": false, 10 | "allowSyntheticDefaultImports": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "experimentalDecorators": true, 13 | "strict": true, 14 | "skipLibCheck": true, 15 | "resolveJsonModule": true, 16 | "noImplicitReturns": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "noUnusedLocals": true, 19 | "outDir": "lib", 20 | "baseUrl": "./", 21 | "plugins": [ 22 | // Transform paths in output .js files 23 | { "transform": "typescript-transform-paths" }, 24 | // Transform paths in output .d.ts files (Include this line if you output declarations files) 25 | { "transform": "typescript-transform-paths", "afterDeclarations": true }, 26 | { 27 | "transform": "typescript-transform-extensions", 28 | "extensions": { ".ts": ".js" } 29 | }, 30 | { 31 | "transform": "../../lib/index.js", 32 | "title": "Fixtures" 33 | } 34 | ] 35 | }, 36 | "include": ["fake"] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "importHelpers": false, 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "sourceMap": false, 10 | "allowSyntheticDefaultImports": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "experimentalDecorators": true, 13 | "strict": true, 14 | "skipLibCheck": true, 15 | "resolveJsonModule": true, 16 | "noImplicitReturns": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "noUnusedLocals": true, 19 | "outDir": "lib", 20 | "baseUrl": "./", 21 | "plugins": [ 22 | // Transform paths in output .js files 23 | { "transform": "typescript-transform-paths" }, 24 | // Transform paths in output .d.ts files (Include this line if you output declarations files) 25 | { "transform": "typescript-transform-paths", "afterDeclarations": true }, 26 | { 27 | "transform": "typescript-transform-extensions", 28 | "extensions": { ".ts": ".js" } 29 | } 30 | ] 31 | }, 32 | "include": ["src"] 33 | } 34 | --------------------------------------------------------------------------------