├── .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 | [](https://github.com/JiangWeixian/tsc-progress/tree/master) [](https://github.com/JiangWeixian/tsc-progress/tree/master)
5 |
6 | 
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 |
--------------------------------------------------------------------------------