├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── .DS_Store ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── stale.yml └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── .vscode └── settings.json ├── .yarn ├── install-state.gz └── releases │ └── yarn-4.7.0.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── changelog.config.js ├── examples ├── babel-loader │ ├── .babelrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── webpack.config.js ├── ts-loader │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── webpack.config.js └── vscode-tasks │ ├── .vscode │ └── tasks.json │ └── README.md ├── media └── logo.svg ├── package.json ├── release.config.js ├── src ├── files-change.ts ├── files-match.ts ├── formatter │ ├── basic-formatter.ts │ ├── code-frame-formatter.ts │ ├── formatter-config.ts │ ├── formatter-options.ts │ ├── formatter.ts │ ├── index.ts │ ├── stats-formatter.ts │ ├── types │ │ └── babel__code-frame.ts │ └── webpack-formatter.ts ├── hooks │ ├── intercept-done-to-get-dev-server-tap.ts │ ├── tap-after-compile-to-add-dependencies.ts │ ├── tap-after-compile-to-get-issues.ts │ ├── tap-after-environment-to-patch-watching.ts │ ├── tap-done-to-async-get-issues.ts │ ├── tap-error-to-log-message.ts │ ├── tap-start-to-run-workers.ts │ └── tap-stop-to-terminate-workers.ts ├── index.ts ├── infrastructure-logger.ts ├── issue │ ├── index.ts │ ├── issue-config.ts │ ├── issue-location.ts │ ├── issue-match.ts │ ├── issue-options.ts │ ├── issue-position.ts │ ├── issue-predicate.ts │ ├── issue-severity.ts │ ├── issue-webpack-error.ts │ └── issue.ts ├── logger.ts ├── plugin-config.ts ├── plugin-hooks.ts ├── plugin-options.json ├── plugin-options.ts ├── plugin-pools.ts ├── plugin-state.ts ├── plugin.ts ├── rpc │ ├── expose-rpc.ts │ ├── index.ts │ ├── rpc-error.ts │ ├── rpc-worker.ts │ ├── types.ts │ └── wrap-rpc.ts ├── typescript │ ├── type-script-config-overwrite.ts │ ├── type-script-diagnostics-options.ts │ ├── type-script-support.ts │ ├── type-script-worker-config.ts │ ├── type-script-worker-options.ts │ └── worker │ │ ├── get-dependencies-worker.ts │ │ ├── get-issues-worker.ts │ │ └── lib │ │ ├── artifacts.ts │ │ ├── config.ts │ │ ├── dependencies.ts │ │ ├── diagnostics.ts │ │ ├── emit.ts │ │ ├── file-system │ │ ├── file-system.ts │ │ ├── mem-file-system.ts │ │ ├── passive-file-system.ts │ │ └── real-file-system.ts │ │ ├── host │ │ ├── compiler-host.ts │ │ ├── watch-compiler-host.ts │ │ └── watch-solution-builder-host.ts │ │ ├── performance.ts │ │ ├── program │ │ ├── program.ts │ │ ├── solution-builder.ts │ │ └── watch-program.ts │ │ ├── system.ts │ │ ├── tracing.ts │ │ ├── tsbuildinfo.ts │ │ ├── typescript.ts │ │ └── worker-config.ts ├── utils │ ├── async │ │ ├── abort-error.ts │ │ ├── controlled-promise.ts │ │ ├── is-pending.ts │ │ ├── pool.ts │ │ └── wait.ts │ └── path │ │ ├── forward-slash.ts │ │ ├── is-inside-another-path.ts │ │ └── relative-to-context.ts └── watch │ ├── inclusive-node-watch-file-system.ts │ └── watch-file-system.ts ├── test ├── .DS_Store ├── e2e │ ├── .DS_Store │ ├── driver │ │ ├── listener.ts │ │ ├── webpack-dev-server-driver.ts │ │ └── webpack-errors-extractor.ts │ ├── fixtures │ │ ├── .DS_Store │ │ ├── type-definitions │ │ │ ├── package.json │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.ts │ │ ├── typescript-basic │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── authenticate.ts │ │ │ │ ├── index.ts │ │ │ │ └── model │ │ │ │ │ ├── Role.ts │ │ │ │ │ └── User.ts │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ ├── typescript-monorepo │ │ │ ├── package.json │ │ │ ├── packages │ │ │ │ ├── client │ │ │ │ │ ├── package.json │ │ │ │ │ ├── src │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ └── shared │ │ │ │ │ ├── package.json │ │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── intersect.ts │ │ │ │ │ └── subtract.ts │ │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig.base.json │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ ├── typescript-package │ │ │ └── package │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ ├── typescript-pnp │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── authenticate.ts │ │ │ │ ├── index.ts │ │ │ │ └── model │ │ │ │ │ ├── Role.ts │ │ │ │ │ └── User.ts │ │ │ ├── tsconfig.json │ │ │ └── webpack.config.js │ │ └── webpack-node-api │ │ │ └── webpack-node-api.js │ ├── jest.config.js │ ├── jest.environment.js │ ├── jest.setup.ts │ ├── out-of-memory-and-cosmiconfig.spec.ts │ ├── tsconfig.json │ ├── type-definitions.spec.ts │ ├── type-script-config.spec.ts │ ├── type-script-context-option.spec.ts │ ├── type-script-formatter-option.spec.ts │ ├── type-script-pnp-support.spec.ts │ ├── type-script-solution-builder-api.spec.ts │ ├── type-script-tracing.spec.ts │ ├── type-script-watch-api.spec.ts │ ├── webpack-inclusive-watcher.spec.ts │ ├── webpack-node-api.spec.ts │ └── webpack-production-build.spec.ts ├── tsconfig.json └── unit │ ├── files-change.spec.ts │ ├── formatter │ ├── __mocks__ │ │ └── chalk.js │ ├── basic-formatter.spec.ts │ ├── code-frame-formatter.spec.ts │ ├── formatter-config.spec.ts │ ├── strip-ansi.ts │ └── webpack-formatter.spec.ts │ ├── issue │ └── issue.spec.ts │ ├── jest.config.js │ ├── plugin.spec.ts │ ├── rpc │ └── wrap-rpc.spec.ts │ ├── typescript │ ├── type-script-support.spec.ts │ └── type-script-worker-config.spec.ts │ └── utils │ ├── async │ └── pool.spec.ts │ └── path │ ├── is-inside-another-path-unix.spec.ts │ └── is-inside-another-path-windows.spec.ts ├── tsconfig.json └── yarn.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | # Avoid warnings by switching to noninteractive 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | # The node image comes with a base non-root 'node' user which this Dockerfile 7 | # gives sudo access. However, for Linux, this user's GID/UID must match your local 8 | # user UID/GID to avoid permission issues with bind mounts. Update USER_UID / USER_GID 9 | # if yours is not 1000. See https://aka.ms/vscode-remote/containers/non-root-user. 10 | ARG USER_UID=1000 11 | ARG USER_GID=$USER_UID 12 | 13 | # Configure apt and install packages 14 | RUN apt-get update \ 15 | && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \ 16 | # 17 | # Verify git and needed tools are installed 18 | && apt-get install -y git procps \ 19 | # 20 | # Remove outdated yarn from /opt and install via package 21 | # so it can be easily updated via apt-get upgrade yarn 22 | && rm -rf /opt/yarn-* \ 23 | && rm -f /usr/local/bin/yarn \ 24 | && rm -f /usr/local/bin/yarnpkg \ 25 | && apt-get install -y curl apt-transport-https lsb-release \ 26 | && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ 27 | && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 28 | && apt-get update \ 29 | && apt-get -y install --no-install-recommends yarn \ 30 | # 31 | # Install eslint globally 32 | && npm install -g eslint \ 33 | # 34 | # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. 35 | && if [ "$USER_GID" != "1000" ]; then groupmod node --gid $USER_GID; fi \ 36 | && if [ "$USER_UID" != "1000" ]; then usermod --uid $USER_UID node; fi \ 37 | # [Optional] Add sudo support for non-root users 38 | && apt-get install -y sudo \ 39 | && echo node ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/node \ 40 | && chmod 0440 /etc/sudoers.d/node \ 41 | # 42 | # Clean up 43 | && apt-get autoremove -y \ 44 | && apt-get clean -y \ 45 | && rm -rf /var/lib/apt/lists/* 46 | 47 | # Switch back to dialog for any ad-hoc use of apt-get 48 | ENV DEBIAN_FRONTEND= 49 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fork-ts-checker-webpack-plugin devcontainer", 3 | "dockerFile": "Dockerfile", 4 | 5 | "user": "node", 6 | 7 | // Comment out the next line to run as root instead. Linux users, update 8 | // Dockerfile with your user's UID/GID if not 1000. 9 | "runArgs": [ "-u", "node" ], 10 | 11 | // Use 'settings' to set *default* container specific settings.json values on container create. 12 | // You can edit these settings after create using File > Preferences > Settings > Remote. 13 | "settings": { 14 | "terminal.integrated.shell.linux": "/bin/bash" 15 | }, 16 | 17 | // Specifies a command that should be run after the container has been created. 18 | "postCreateCommand": "yarn install", 19 | 20 | "extensions": [ 21 | "dbaeumer.vscode-eslint" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib/** 2 | /test/e2e/fixtures/** 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'import', 'prettier'], 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'plugin:node/recommended', 9 | ], 10 | settings: { 11 | node: { 12 | tryExtensions: ['.js', '.json', '.ts', '.d.ts'], 13 | }, 14 | 'import/extensions': ['.js', '.json', '.ts', '.d.ts'], 15 | 'import/external-module-folders': ['node_modules', 'node_modules/@types'], 16 | 'import/parsers': { 17 | '@typescript-eslint/parser': ['.ts'], 18 | }, 19 | 'import/resolver': { 20 | node: { 21 | extensions: ['.js', '.json', '.ts', '.d.ts'], 22 | }, 23 | }, 24 | }, 25 | rules: { 26 | '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], 27 | 'import/order': [ 28 | 'error', 29 | { 30 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'], 31 | 'newlines-between': 'always', 32 | alphabetize: { 33 | order: 'asc', 34 | caseInsensitive: true, 35 | }, 36 | }, 37 | ], 38 | 'import/no-cycle': ['error'], 39 | 'prettier/prettier': 'error', 40 | }, 41 | overrides: [ 42 | { 43 | files: ['*.ts'], 44 | rules: { 45 | '@typescript-eslint/explicit-function-return-type': 'off', 46 | '@typescript-eslint/explicit-module-boundary-types': 'off', 47 | '@typescript-eslint/no-use-before-define': 'off', 48 | 'node/no-unsupported-features/es-syntax': 'off', 49 | }, 50 | }, 51 | { 52 | files: ['*.spec.ts'], 53 | rules: { 54 | '@typescript-eslint/no-var-requires': 'off', 55 | 'node/no-missing-import': 'off', 56 | }, 57 | }, 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TypeStrong/fork-ts-checker-webpack-plugin/9f70a3dcdaf216177b9b7b85426fc8e473bfad4e/.github/.DS_Store -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: piotr-oles 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Current behavior 11 | 12 | 13 | ## Expected behavior 14 | 15 | 16 | ## Steps to reproduce the issue 17 | 18 | 19 | ## Issue reproduction repository 20 | 21 | 22 | ## Environment 23 | - **fork-ts-checker-webpack-plugin**: [version from the `package.json`] 24 | - **typescript**: [version from the `package.json`] 25 | - **eslint**: [version from the `package.json`] 26 | - **webpack**: [version from the `package.json`] 27 | - **os**: [e.g. Ubuntu 19.04] 28 | -------------------------------------------------------------------------------- /.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 | ## Feature motivation 11 | 12 | 13 | ## Feature description 14 | 15 | 16 | ## Feature implementation 17 | 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: [push, pull_request] 3 | permissions: 4 | contents: read 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Setup node 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | cache: 'yarn' 16 | 17 | - name: Install dependencies 18 | run: yarn install --immutable 19 | 20 | - name: Build project 21 | run: yarn build 22 | 23 | - name: Upload build artifact 24 | uses: actions/upload-artifact@v4 25 | with: 26 | name: lib 27 | path: lib 28 | 29 | test: 30 | runs-on: ${{ matrix.os }} 31 | needs: build 32 | strategy: 33 | matrix: 34 | node: [18, 20] 35 | os: [ubuntu-latest, macos-latest, windows-latest] 36 | fail-fast: false 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: Setup node 41 | uses: actions/setup-node@v4 42 | with: 43 | node-version: ${{ matrix.node }} 44 | cache: 'yarn' 45 | 46 | - name: Locks cache 47 | uses: actions/cache@v4 48 | with: 49 | path: test/e2e/__locks__ 50 | key: ${{ runner.os }}-locks 51 | 52 | - name: Install dependencies 53 | run: yarn install --immutable 54 | 55 | - name: Download build artifact 56 | uses: actions/download-artifact@v4 57 | with: 58 | name: lib 59 | path: lib 60 | 61 | - name: Run unit tests 62 | run: yarn test:unit 63 | 64 | - name: Run e2e tests 65 | run: yarn test:e2e 66 | 67 | release: 68 | runs-on: ubuntu-latest 69 | permissions: 70 | issues: write 71 | contents: write 72 | pull-requests: write 73 | deployments: write 74 | needs: [build, test] 75 | if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/alpha' || github.ref == 'refs/heads/beta') 76 | steps: 77 | - uses: actions/checkout@v4 78 | 79 | - name: Setup node 80 | uses: actions/setup-node@v4 81 | with: 82 | node-version: 20 83 | cache: 'yarn' 84 | 85 | - name: Install dependencies 86 | run: yarn install --immutable 87 | 88 | - name: Download build artifact 89 | uses: actions/download-artifact@v4 90 | with: 91 | name: lib 92 | path: lib 93 | 94 | - name: Release 95 | run: yarn semantic-release 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Package artifacts 5 | /lib 6 | 7 | # Package archive used by e2e tests 8 | fork-ts-checker-webpack-plugin-0.0.0-semantic-release.tgz 9 | 10 | # E2E tests lock file cache dir 11 | __locks__ 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Dependency directories 17 | node_modules 18 | 19 | # Editor directories and files 20 | .idea 21 | 22 | # Mac OS 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged && yarn build && yarn test:unit 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact" 7 | ], 8 | "jest.virtualFolders": [ 9 | { 10 | "name": "test/e2e", 11 | "jestCommandLine": "npm pack && yarn jest --config=test/e2e/jest.config.js --ci -i -b", 12 | "runMode": "on-demand" 13 | }, 14 | { 15 | "name": "test/unit", 16 | "jestCommandLine": "yarn jest --config=test/unit/jest.config.js", 17 | "runMode": "on-demand" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TypeStrong/fork-ts-checker-webpack-plugin/9f70a3dcdaf216177b9b7b85426fc8e473bfad4e/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.7.0.cjs 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, 7 | body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, 8 | race, religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail address, 41 | posting via an official social media account, or acting as an appointed representative at an online or offline event. 42 | Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | piotrek.oles@gmail.com. The project team will review and investigate all complaints, and will respond 48 | in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality 49 | with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 57 | available at [http://contributor-covenant.org/version/1/4][version] 58 | 59 | [homepage]: http://contributor-covenant.org 60 | [version]: http://contributor-covenant.org/version/1/4/ 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TypeStrong 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 | -------------------------------------------------------------------------------- /changelog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | list: ['feat', 'fix', 'refactor', 'perf', 'test', 'chore', 'docs'], 3 | maxMessageLength: 64, 4 | minMessageLength: 3, 5 | questions: ['type', 'subject', 'body', 'breaking', 'issues'], 6 | types: { 7 | feat: { 8 | description: 'A new feature', 9 | value: 'feat', 10 | section: 'Features', 11 | }, 12 | fix: { 13 | description: 'A bug fix', 14 | value: 'fix', 15 | section: 'Bug Fixes', 16 | }, 17 | refactor: { 18 | description: 'A code change that neither adds a feature or fixes a bug', 19 | value: 'refactor', 20 | hidden: true, 21 | }, 22 | perf: { 23 | description: 'A code change that improves performance', 24 | value: 'perf', 25 | hidden: true, 26 | }, 27 | test: { 28 | description: 'Adding missing tests', 29 | value: 'test', 30 | hidden: true, 31 | }, 32 | chore: { 33 | description: 'Build process, CI or auxiliary tool changes', 34 | value: 'chore', 35 | hidden: true, 36 | }, 37 | docs: { 38 | description: 'Documentation only changes', 39 | value: 'docs', 40 | hidden: true, 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /examples/babel-loader/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', ['@babel/preset-typescript']], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/babel-loader/README.md: -------------------------------------------------------------------------------- 1 | ## babel-loader configuration example 2 | 3 | It's a basic configuration of the plugin and [babel-loader](https://github.com/babel/babel-loader). 4 | Very similar to the [ts-loader example](../ts-loader), the main difference in the configuration is that we 5 | enable **syntactic diagnostics**: 6 | 7 | ```js 8 | new ForkTsCheckerWebpackPlugin({ 9 | typescript: { 10 | diagnosticOptions: { 11 | semantic: true, 12 | syntactic: true, 13 | }, 14 | }, 15 | }) 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /examples/babel-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fork-ts-checker-webpack-plugin-babel-loader-example", 3 | "version": "0.0.0", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "webpack serve --mode=development", 8 | "build": "webpack --mode=production" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.6.0", 12 | "@babel/preset-env": "^7.6.0", 13 | "@babel/preset-typescript": "^7.6.0", 14 | "babel-loader": "^8.0.0", 15 | "fork-ts-checker-webpack-plugin": "^7.2.8", 16 | "typescript": "^4.6.4", 17 | "webpack": "^5.72.0", 18 | "webpack-cli": "^4.9.2", 19 | "webpack-dev-server": "^4.8.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/babel-loader/src/index.ts: -------------------------------------------------------------------------------- 1 | console.log('Hello world'); 2 | -------------------------------------------------------------------------------- /examples/babel-loader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "CommonJS", 5 | "lib": ["ES5", "ScriptHost"], 6 | "moduleResolution": "Node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "skipDefaultLibCheck": true, 10 | "strict": true, 11 | "baseUrl": "./src", 12 | "outDir": "./dist" 13 | }, 14 | "include": ["./src"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /examples/babel-loader/webpack.config.js: -------------------------------------------------------------------------------- 1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: './src/index.ts', 6 | resolve: { 7 | extensions: ['.ts', '.tsx', '.js'], 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | loader: 'babel-loader', 14 | }, 15 | ], 16 | }, 17 | plugins: [ 18 | new ForkTsCheckerWebpackPlugin({ 19 | typescript: { 20 | diagnosticOptions: { 21 | semantic: true, 22 | syntactic: true, 23 | }, 24 | mode: 'write-references', 25 | }, 26 | }), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /examples/ts-loader/README.md: -------------------------------------------------------------------------------- 1 | ## ts-loader configuration example 2 | 3 | It's a basic configuration of the plugin and [ts-loader](https://github.com/TypeStrong/ts-loader). 4 | -------------------------------------------------------------------------------- /examples/ts-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fork-ts-checker-webpack-plugin-ts-loader-example", 3 | "version": "0.0.0", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "webpack serve --mode=development", 8 | "build": "webpack --mode=production" 9 | }, 10 | "devDependencies": { 11 | "fork-ts-checker-webpack-plugin": "^7.2.8", 12 | "ts-loader": "^9.2.9", 13 | "typescript": "^4.6.4", 14 | "webpack": "^5.72.0", 15 | "webpack-cli": "^4.9.2", 16 | "webpack-dev-server": "^4.8.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/ts-loader/src/index.ts: -------------------------------------------------------------------------------- 1 | console.log('Hello world'); 2 | -------------------------------------------------------------------------------- /examples/ts-loader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "CommonJS", 5 | "lib": ["ES5", "ScriptHost"], 6 | "moduleResolution": "Node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "skipDefaultLibCheck": true, 10 | "strict": true, 11 | "baseUrl": "./src", 12 | "outDir": "./dist", 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "include": ["./src"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/ts-loader/webpack.config.js: -------------------------------------------------------------------------------- 1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: './src/index.ts', 6 | resolve: { 7 | extensions: ['.ts', '.tsx', '.js'], 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | loader: 'ts-loader', 14 | }, 15 | ], 16 | }, 17 | plugins: [new ForkTsCheckerWebpackPlugin()], 18 | }; 19 | -------------------------------------------------------------------------------- /examples/vscode-tasks/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "group": "build", 8 | "problemMatcher": ["$ts-checker5-webpack", "$ts-checker5-eslint-webpack"] 9 | }, 10 | { 11 | "type": "npm", 12 | "script": "lint", 13 | "group": "build", 14 | "problemMatcher": ["$eslint-stylish"] 15 | }, 16 | { 17 | "type": "npm", 18 | "script": "watch", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | "isBackground": true, 24 | "problemMatcher": ["$ts-checker5-webpack-watch", "$ts-checker5-eslint-webpack-watch"] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/vscode-tasks/README.md: -------------------------------------------------------------------------------- 1 | ## Visual Studio Code configuration example 2 | 3 | This example defines `.vscode/tasks.json` file which instructs **Visual Studio Code** how to extract errors from the webpack's output 4 | to display them in the **Problems** tab. It uses [TypeScript + Webpack Problem Matchers](https://marketplace.visualstudio.com/items?itemName=eamodio.tsl-problem-matcher) 5 | provided by @eamodio :heart: 6 | 7 | > Tip: You can use the npm type tasks even with yarn if you set "npm.packageManager": "yarn" in your Visual Studio Code settings 8 | -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fork-ts-checker-webpack-plugin", 3 | "version": "0.0.0-semantic-release", 4 | "description": "Runs typescript type checker and linter on separate process.", 5 | "keywords": [ 6 | "webpack", 7 | "plugin", 8 | "typescript", 9 | "typecheck", 10 | "ts-loader", 11 | "webpack", 12 | "fork", 13 | "fast" 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/TypeStrong/fork-ts-checker-webpack-plugin.git" 21 | }, 22 | "license": "MIT", 23 | "author": "Piotr Oleś ", 24 | "contributors": [ 25 | "Piotr Oleś (https://github.com/piotr-oles)", 26 | "John Reilly (https://johnnyreilly.com)" 27 | ], 28 | "files": [ 29 | "lib" 30 | ], 31 | "main": "lib/index.js", 32 | "types": "lib/index.d.ts", 33 | "scripts": { 34 | "build": "cross-env rimraf lib && cross-env tsc --version && cross-env tsc", 35 | "lint": "cross-env eslint ./src ./test --ext .ts", 36 | "test": "yarn build && yarn test:unit && yarn test:e2e", 37 | "test:unit": "cross-env jest --config=test/unit/jest.config.js", 38 | "test:e2e": "npm pack && cross-env YARN_ENABLE_IMMUTABLE_INSTALLS=false jest --config=test/e2e/jest.config.js --ci -i -b", 39 | "precommit": "cross-env lint-staged && yarn build && yarn test:unit", 40 | "commit": "cross-env git-cz", 41 | "semantic-release": "semantic-release", 42 | "prepare": "husky install" 43 | }, 44 | "commitlint": { 45 | "extends": [ 46 | "@commitlint/config-conventional" 47 | ] 48 | }, 49 | "lint-staged": { 50 | "*.ts": "eslint --fix" 51 | }, 52 | "config": { 53 | "commitizen": { 54 | "path": "cz-conventional-changelog" 55 | } 56 | }, 57 | "dependencies": { 58 | "@babel/code-frame": "^7.16.7", 59 | "chalk": "^4.1.2", 60 | "chokidar": "^4.0.1", 61 | "cosmiconfig": "^8.2.0", 62 | "deepmerge": "^4.2.2", 63 | "fs-extra": "^10.0.0", 64 | "memfs": "^3.4.1", 65 | "minimatch": "^3.0.4", 66 | "node-abort-controller": "^3.0.1", 67 | "schema-utils": "^3.1.1", 68 | "semver": "^7.3.5", 69 | "tapable": "^2.2.1" 70 | }, 71 | "peerDependencies": { 72 | "typescript": ">3.6.0", 73 | "webpack": "^5.11.0" 74 | }, 75 | "devDependencies": { 76 | "@commitlint/config-conventional": "^16.0.0", 77 | "@semantic-release/commit-analyzer": "^8.0.1", 78 | "@semantic-release/exec": "^5.0.0", 79 | "@semantic-release/github": "^7.2.3", 80 | "@semantic-release/npm": "^7.1.3", 81 | "@semantic-release/release-notes-generator": "^9.0.3", 82 | "@types/babel__code-frame": "^7.0.3", 83 | "@types/cross-spawn": "^6.0.2", 84 | "@types/fs-extra": "^9.0.13", 85 | "@types/jest": "^27.4.0", 86 | "@types/json-schema": "^7.0.9", 87 | "@types/minimatch": "^3.0.5", 88 | "@types/mock-fs": "^4.13.1", 89 | "@types/node": "^16.4.13", 90 | "@types/rimraf": "^3.0.2", 91 | "@types/semver": "^7.3.9", 92 | "@typescript-eslint/eslint-plugin": "^5.10.1", 93 | "@typescript-eslint/parser": "^5.10.1", 94 | "commitlint": "^16.1.0", 95 | "cross-env": "^7.0.3", 96 | "eslint": "^8.8.0", 97 | "eslint-plugin-import": "^2.25.4", 98 | "eslint-plugin-node": "^11.1.0", 99 | "eslint-plugin-prettier": "^4.0.0", 100 | "git-cz": "^4.8.0", 101 | "husky": "^7.0.4", 102 | "jest": "^27.4.7", 103 | "jest-circus": "^27.4.6", 104 | "jest-environment-node": "^27.4.6", 105 | "json-schema": "^0.4.0", 106 | "karton": "^0.4.1", 107 | "lint-staged": "^11.1.2", 108 | "mock-fs": "^5.1.2", 109 | "prettier": "^2.5.1", 110 | "rimraf": "^3.0.2", 111 | "semantic-release": "^17.4.4", 112 | "strip-ansi": "^6.0.0", 113 | "ts-jest": "^27.1.3", 114 | "typescript": "^4.5.5", 115 | "webpack": "^5.67.0" 116 | }, 117 | "engines": { 118 | "node": ">=14.21.3" 119 | }, 120 | "packageManager": "yarn@4.7.0" 121 | } 122 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | 'main', 4 | { 5 | name: 'alpha', 6 | prerelease: true, 7 | }, 8 | { 9 | name: 'beta', 10 | prerelease: true, 11 | }, 12 | ], 13 | plugins: [ 14 | [ 15 | '@semantic-release/commit-analyzer', 16 | { 17 | preset: 'angular', 18 | releaseRules: [ 19 | { breaking: true, release: 'major' }, 20 | { revert: true, release: 'patch' }, 21 | { type: 'feat', release: 'minor' }, 22 | { type: 'fix', release: 'patch' }, 23 | { type: 'perf', release: 'patch' }, 24 | { type: 'refactor', release: 'patch' }, 25 | { type: 'docs', release: 'patch' }, 26 | ], 27 | }, 28 | ], 29 | '@semantic-release/release-notes-generator', 30 | '@semantic-release/npm', 31 | '@semantic-release/github', 32 | [ 33 | '@semantic-release/exec', 34 | { 35 | prepareCmd: "sed -i 's/{{VERSION}}/${nextRelease.version}/g' lib/plugin.js", 36 | }, 37 | ], 38 | ], 39 | }; 40 | -------------------------------------------------------------------------------- /src/files-change.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | interface FilesChange { 4 | changedFiles?: string[]; 5 | deletedFiles?: string[]; 6 | } 7 | 8 | // we ignore package.json file because of https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/issues/674 9 | const IGNORED_FILES = ['package.json']; 10 | 11 | const isIgnoredFile = (file: string) => 12 | IGNORED_FILES.some( 13 | (ignoredFile) => file.endsWith(`/${ignoredFile}`) || file.endsWith(`\\${ignoredFile}`) 14 | ); 15 | 16 | const compilerFilesChangeMap = new WeakMap(); 17 | 18 | function getFilesChange(compiler: webpack.Compiler): FilesChange { 19 | const { changedFiles = [], deletedFiles = [] } = compilerFilesChangeMap.get(compiler) || { 20 | changedFiles: [], 21 | deletedFiles: [], 22 | }; 23 | 24 | return { 25 | changedFiles: changedFiles.filter((changedFile) => !isIgnoredFile(changedFile)), 26 | deletedFiles: deletedFiles.filter((deletedFile) => !isIgnoredFile(deletedFile)), 27 | }; 28 | } 29 | 30 | function consumeFilesChange(compiler: webpack.Compiler): FilesChange { 31 | const change = getFilesChange(compiler); 32 | clearFilesChange(compiler); 33 | return change; 34 | } 35 | 36 | function updateFilesChange(compiler: webpack.Compiler, change: FilesChange): void { 37 | compilerFilesChangeMap.set(compiler, aggregateFilesChanges([getFilesChange(compiler), change])); 38 | } 39 | 40 | function clearFilesChange(compiler: webpack.Compiler): void { 41 | compilerFilesChangeMap.delete(compiler); 42 | } 43 | 44 | /** 45 | * Computes aggregated files change based on the subsequent files changes. 46 | * 47 | * @param changes List of subsequent files changes 48 | * @returns Files change that represents all subsequent changes as a one event 49 | */ 50 | function aggregateFilesChanges(changes: FilesChange[]): FilesChange { 51 | const changedFilesSet = new Set(); 52 | const deletedFilesSet = new Set(); 53 | 54 | for (const { changedFiles = [], deletedFiles = [] } of changes) { 55 | for (const changedFile of changedFiles) { 56 | changedFilesSet.add(changedFile); 57 | deletedFilesSet.delete(changedFile); 58 | } 59 | for (const deletedFile of deletedFiles) { 60 | changedFilesSet.delete(deletedFile); 61 | deletedFilesSet.add(deletedFile); 62 | } 63 | } 64 | 65 | return { 66 | changedFiles: Array.from(changedFilesSet), 67 | deletedFiles: Array.from(deletedFilesSet), 68 | }; 69 | } 70 | 71 | export { 72 | FilesChange, 73 | getFilesChange, 74 | consumeFilesChange, 75 | updateFilesChange, 76 | clearFilesChange, 77 | aggregateFilesChanges, 78 | }; 79 | -------------------------------------------------------------------------------- /src/files-match.ts: -------------------------------------------------------------------------------- 1 | interface FilesMatch { 2 | files: string[]; 3 | dirs: string[]; 4 | excluded: string[]; 5 | extensions: string[]; 6 | } 7 | 8 | export { FilesMatch }; 9 | -------------------------------------------------------------------------------- /src/formatter/basic-formatter.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import type { Formatter } from './formatter'; 4 | 5 | function createBasicFormatter(): Formatter { 6 | return function basicFormatter(issue) { 7 | return chalk.grey(issue.code + ': ') + issue.message; 8 | }; 9 | } 10 | 11 | export { createBasicFormatter }; 12 | -------------------------------------------------------------------------------- /src/formatter/code-frame-formatter.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import { codeFrameColumns } from '@babel/code-frame'; 4 | import fs from 'fs-extra'; 5 | 6 | import { createBasicFormatter } from './basic-formatter'; 7 | import type { Formatter } from './formatter'; 8 | import { BabelCodeFrameOptions } from './types/babel__code-frame'; 9 | 10 | function createCodeFrameFormatter(options?: BabelCodeFrameOptions): Formatter { 11 | const basicFormatter = createBasicFormatter(); 12 | 13 | return function codeFrameFormatter(issue) { 14 | const source = issue.file && fs.existsSync(issue.file) && fs.readFileSync(issue.file, 'utf-8'); 15 | 16 | let frame = ''; 17 | if (source && issue.location) { 18 | frame = codeFrameColumns(source, issue.location, { 19 | highlightCode: true, 20 | ...(options || {}), 21 | }) 22 | .split('\n') 23 | .map((line) => ' ' + line) 24 | .join(os.EOL); 25 | } 26 | 27 | const lines = [basicFormatter(issue)]; 28 | if (frame) { 29 | lines.push(frame); 30 | } 31 | 32 | return lines.join(os.EOL); 33 | }; 34 | } 35 | 36 | export { createCodeFrameFormatter, BabelCodeFrameOptions }; 37 | -------------------------------------------------------------------------------- /src/formatter/formatter-config.ts: -------------------------------------------------------------------------------- 1 | import { createBasicFormatter } from './basic-formatter'; 2 | import { createCodeFrameFormatter } from './code-frame-formatter'; 3 | import type { Formatter, FormatterPathType } from './formatter'; 4 | import type { CodeframeFormatterOptions, FormatterOptions } from './formatter-options'; 5 | 6 | type FormatterConfig = { 7 | format: Formatter; 8 | pathType: FormatterPathType; 9 | }; 10 | 11 | function createFormatterConfig(options: FormatterOptions | undefined): FormatterConfig { 12 | if (typeof options === 'function') { 13 | return { 14 | format: options, 15 | pathType: 'relative', 16 | }; 17 | } 18 | 19 | const type = options 20 | ? typeof options === 'object' 21 | ? options.type || 'codeframe' 22 | : options 23 | : 'codeframe'; 24 | const pathType = 25 | options && typeof options === 'object' ? options.pathType || 'relative' : 'relative'; 26 | 27 | if (!type || type === 'basic') { 28 | return { 29 | format: createBasicFormatter(), 30 | pathType, 31 | }; 32 | } 33 | 34 | if (type === 'codeframe') { 35 | const config = 36 | options && typeof options === 'object' 37 | ? (options as CodeframeFormatterOptions).options || {} 38 | : {}; 39 | 40 | return { 41 | format: createCodeFrameFormatter(config), 42 | pathType, 43 | }; 44 | } 45 | 46 | throw new Error( 47 | `Unknown "${type}" formatter. Available types are: "basic", "codeframe" or a custom function.` 48 | ); 49 | } 50 | 51 | export { FormatterConfig, createFormatterConfig }; 52 | -------------------------------------------------------------------------------- /src/formatter/formatter-options.ts: -------------------------------------------------------------------------------- 1 | import type { Formatter, FormatterPathType } from './formatter'; 2 | import type { BabelCodeFrameOptions } from './types/babel__code-frame'; 3 | 4 | type FormatterType = 'basic' | 'codeframe'; 5 | 6 | type BasicFormatterOptions = { 7 | type: 'basic'; 8 | pathType?: FormatterPathType; 9 | }; 10 | type CodeframeFormatterOptions = { 11 | type: 'codeframe'; 12 | pathType?: FormatterPathType; 13 | options?: BabelCodeFrameOptions; 14 | }; 15 | type FormatterOptions = 16 | | undefined 17 | | FormatterType 18 | | BasicFormatterOptions 19 | | CodeframeFormatterOptions 20 | | Formatter; 21 | 22 | export { FormatterOptions, FormatterType, BasicFormatterOptions, CodeframeFormatterOptions }; 23 | -------------------------------------------------------------------------------- /src/formatter/formatter.ts: -------------------------------------------------------------------------------- 1 | import type { Issue } from '../issue'; 2 | 3 | type Formatter = (issue: Issue) => string; 4 | type FormatterPathType = 'relative' | 'absolute'; 5 | 6 | export { Formatter, FormatterPathType }; 7 | -------------------------------------------------------------------------------- /src/formatter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './formatter'; 2 | export * from './basic-formatter'; 3 | export * from './code-frame-formatter'; 4 | export * from './webpack-formatter'; 5 | export * from './formatter-options'; 6 | export * from './formatter-config'; 7 | -------------------------------------------------------------------------------- /src/formatter/stats-formatter.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import type { Stats } from 'webpack'; 3 | 4 | import type { Issue } from '../issue'; 5 | 6 | // mimics webpack's stats summary formatter 7 | export function statsFormatter(issues: Issue[], stats: Stats): string { 8 | const errorsNumber = issues.filter((issue) => issue.severity === 'error').length; 9 | const warningsNumber = issues.filter((issue) => issue.severity === 'warning').length; 10 | const errorsFormatted = errorsNumber 11 | ? chalk.red.bold(`${errorsNumber} ${errorsNumber === 1 ? 'error' : 'errors'}`) 12 | : ''; 13 | const warningsFormatted = warningsNumber 14 | ? chalk.yellow.bold(`${warningsNumber} ${warningsNumber === 1 ? 'warning' : 'warnings'}`) 15 | : ''; 16 | const timeFormatted = Math.round(Date.now() - stats.startTime); 17 | 18 | return [ 19 | 'Found ', 20 | errorsFormatted, 21 | errorsFormatted && warningsFormatted ? ' and ' : '', 22 | warningsFormatted, 23 | ` in ${timeFormatted} ms`, 24 | '.', 25 | ].join(''); 26 | } 27 | -------------------------------------------------------------------------------- /src/formatter/types/babel__code-frame.ts: -------------------------------------------------------------------------------- 1 | // Base on the type definitions for @babel/code-frame 7.0 2 | // Project: https://github.com/babel/babel/tree/main/packages/babel-code-frame, https://babeljs.io 3 | // Definitions by: Mohsen Azimi 4 | // Forbes Lindesay 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | 7 | export interface BabelCodeFrameOptions { 8 | /** Syntax highlight the code as JavaScript for terminals. default: false */ 9 | highlightCode?: boolean; 10 | /** The number of lines to show above the error. default: 2 */ 11 | linesAbove?: number; 12 | /** The number of lines to show below the error. default: 3 */ 13 | linesBelow?: number; 14 | /** 15 | * Forcibly syntax highlight the code as JavaScript (for non-terminals); 16 | * overrides highlightCode. 17 | * default: false 18 | */ 19 | forceColor?: boolean; 20 | /** 21 | * Pass in a string to be displayed inline (if possible) next to the 22 | * highlighted location in the code. If it can't be positioned inline, 23 | * it will be placed above the code frame. 24 | * default: nothing 25 | */ 26 | message?: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/formatter/webpack-formatter.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import path from 'path'; 3 | 4 | import chalk from 'chalk'; 5 | 6 | import { formatIssueLocation } from '../issue'; 7 | import { forwardSlash } from '../utils/path/forward-slash'; 8 | import { relativeToContext } from '../utils/path/relative-to-context'; 9 | 10 | import type { Formatter, FormatterPathType } from './formatter'; 11 | 12 | function createWebpackFormatter(formatter: Formatter, pathType: FormatterPathType): Formatter { 13 | // mimics webpack error formatter 14 | return function webpackFormatter(issue) { 15 | const color = issue.severity === 'warning' ? chalk.yellow.bold : chalk.red.bold; 16 | 17 | const severity = issue.severity.toUpperCase(); 18 | 19 | if (issue.file) { 20 | let location = chalk.bold( 21 | pathType === 'absolute' 22 | ? forwardSlash(path.resolve(issue.file)) 23 | : relativeToContext(issue.file, process.cwd()) 24 | ); 25 | if (issue.location) { 26 | location += `:${chalk.green.bold(formatIssueLocation(issue.location))}`; 27 | } 28 | 29 | return [`${color(severity)} in ${location}`, formatter(issue), ''].join(os.EOL); 30 | } else { 31 | return [`${color(severity)} in ` + formatter(issue), ''].join(os.EOL); 32 | } 33 | }; 34 | } 35 | 36 | export { createWebpackFormatter }; 37 | -------------------------------------------------------------------------------- /src/hooks/intercept-done-to-get-dev-server-tap.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | import { getInfrastructureLogger } from '../infrastructure-logger'; 4 | import type { ForkTsCheckerWebpackPluginConfig } from '../plugin-config'; 5 | import type { ForkTsCheckerWebpackPluginState } from '../plugin-state'; 6 | 7 | function interceptDoneToGetDevServerTap( 8 | compiler: webpack.Compiler, 9 | config: ForkTsCheckerWebpackPluginConfig, 10 | state: ForkTsCheckerWebpackPluginState 11 | ) { 12 | const { debug } = getInfrastructureLogger(compiler); 13 | 14 | // inspired by https://github.com/ypresto/fork-ts-checker-async-overlay-webpack-plugin 15 | compiler.hooks.done.intercept({ 16 | register: (tap) => { 17 | if (tap.name === 'webpack-dev-server' && tap.type === 'sync' && config.devServer) { 18 | debug('Intercepting webpack-dev-server tap.'); 19 | state.webpackDevServerDoneTap = tap; 20 | } 21 | return tap; 22 | }, 23 | }); 24 | } 25 | 26 | export { interceptDoneToGetDevServerTap }; 27 | -------------------------------------------------------------------------------- /src/hooks/tap-after-compile-to-add-dependencies.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | import { getInfrastructureLogger } from '../infrastructure-logger'; 4 | import type { ForkTsCheckerWebpackPluginConfig } from '../plugin-config'; 5 | import type { ForkTsCheckerWebpackPluginState } from '../plugin-state'; 6 | 7 | function tapAfterCompileToAddDependencies( 8 | compiler: webpack.Compiler, 9 | config: ForkTsCheckerWebpackPluginConfig, 10 | state: ForkTsCheckerWebpackPluginState 11 | ) { 12 | const { debug } = getInfrastructureLogger(compiler); 13 | 14 | compiler.hooks.afterCompile.tapPromise('ForkTsCheckerWebpackPlugin', async (compilation) => { 15 | if (compilation.compiler !== compiler) { 16 | // run only for the compiler that the plugin was registered for 17 | return; 18 | } 19 | 20 | const dependencies = await state.dependenciesPromise; 21 | 22 | debug(`Got dependencies from the getDependenciesWorker.`, dependencies); 23 | if (dependencies) { 24 | state.lastDependencies = dependencies; 25 | 26 | dependencies.files.forEach((file) => { 27 | compilation.fileDependencies.add(file); 28 | }); 29 | } 30 | }); 31 | } 32 | 33 | export { tapAfterCompileToAddDependencies }; 34 | -------------------------------------------------------------------------------- /src/hooks/tap-after-compile-to-get-issues.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | import { getInfrastructureLogger } from '../infrastructure-logger'; 4 | import type { Issue } from '../issue'; 5 | import { IssueWebpackError } from '../issue/issue-webpack-error'; 6 | import type { ForkTsCheckerWebpackPluginConfig } from '../plugin-config'; 7 | import { getPluginHooks } from '../plugin-hooks'; 8 | import type { ForkTsCheckerWebpackPluginState } from '../plugin-state'; 9 | 10 | function tapAfterCompileToGetIssues( 11 | compiler: webpack.Compiler, 12 | config: ForkTsCheckerWebpackPluginConfig, 13 | state: ForkTsCheckerWebpackPluginState 14 | ) { 15 | const hooks = getPluginHooks(compiler); 16 | const { debug } = getInfrastructureLogger(compiler); 17 | 18 | compiler.hooks.afterCompile.tapPromise('ForkTsCheckerWebpackPlugin', async (compilation) => { 19 | if (compilation.compiler !== compiler) { 20 | // run only for the compiler that the plugin was registered for 21 | return; 22 | } 23 | 24 | let issues: Issue[] | undefined = []; 25 | 26 | try { 27 | issues = await state.issuesPromise; 28 | } catch (error) { 29 | hooks.error.call(error, compilation); 30 | return; 31 | } 32 | 33 | debug('Got issues from getIssuesWorker.', issues?.length); 34 | 35 | if (!issues) { 36 | // some error has been thrown or it was canceled 37 | return; 38 | } 39 | 40 | // filter list of issues by provided issue predicate 41 | issues = issues.filter(config.issue.predicate); 42 | 43 | // modify list of issues in the plugin hooks 44 | issues = hooks.issues.call(issues, compilation); 45 | 46 | issues.forEach((issue) => { 47 | const error = new IssueWebpackError( 48 | config.formatter.format(issue), 49 | config.formatter.pathType, 50 | issue 51 | ); 52 | 53 | if (issue.severity === 'warning') { 54 | compilation.warnings.push(error); 55 | } else { 56 | compilation.errors.push(error); 57 | } 58 | }); 59 | }); 60 | } 61 | 62 | export { tapAfterCompileToGetIssues }; 63 | -------------------------------------------------------------------------------- /src/hooks/tap-after-environment-to-patch-watching.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | import { getInfrastructureLogger } from '../infrastructure-logger'; 4 | import type { ForkTsCheckerWebpackPluginState } from '../plugin-state'; 5 | import { InclusiveNodeWatchFileSystem } from '../watch/inclusive-node-watch-file-system'; 6 | import type { WatchFileSystem } from '../watch/watch-file-system'; 7 | 8 | function tapAfterEnvironmentToPatchWatching( 9 | compiler: webpack.Compiler, 10 | state: ForkTsCheckerWebpackPluginState 11 | ) { 12 | const { debug } = getInfrastructureLogger(compiler); 13 | 14 | compiler.hooks.afterEnvironment.tap('ForkTsCheckerWebpackPlugin', () => { 15 | const watchFileSystem = compiler.watchFileSystem; 16 | if (watchFileSystem) { 17 | debug("Overwriting webpack's watch file system."); 18 | // wrap original watch file system 19 | compiler.watchFileSystem = new InclusiveNodeWatchFileSystem( 20 | // we use some internals here 21 | watchFileSystem as WatchFileSystem, 22 | compiler, 23 | state 24 | ); 25 | } else { 26 | debug('No watch file system found - plugin may not work correctly.'); 27 | } 28 | }); 29 | } 30 | 31 | export { tapAfterEnvironmentToPatchWatching }; 32 | -------------------------------------------------------------------------------- /src/hooks/tap-done-to-async-get-issues.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import type * as webpack from 'webpack'; 3 | 4 | import { statsFormatter } from '../formatter/stats-formatter'; 5 | import { createWebpackFormatter } from '../formatter/webpack-formatter'; 6 | import { getInfrastructureLogger } from '../infrastructure-logger'; 7 | import type { Issue } from '../issue'; 8 | import { IssueWebpackError } from '../issue/issue-webpack-error'; 9 | import type { ForkTsCheckerWebpackPluginConfig } from '../plugin-config'; 10 | import { getPluginHooks } from '../plugin-hooks'; 11 | import type { ForkTsCheckerWebpackPluginState } from '../plugin-state'; 12 | import { isPending } from '../utils/async/is-pending'; 13 | import { wait } from '../utils/async/wait'; 14 | 15 | function tapDoneToAsyncGetIssues( 16 | compiler: webpack.Compiler, 17 | config: ForkTsCheckerWebpackPluginConfig, 18 | state: ForkTsCheckerWebpackPluginState 19 | ) { 20 | const hooks = getPluginHooks(compiler); 21 | const { debug } = getInfrastructureLogger(compiler); 22 | 23 | compiler.hooks.done.tap('ForkTsCheckerWebpackPlugin', async (stats) => { 24 | if (stats.compilation.compiler !== compiler) { 25 | // run only for the compiler that the plugin was registered for 26 | return; 27 | } 28 | 29 | const issuesPromise = state.issuesPromise; 30 | let issues: Issue[] | undefined; 31 | 32 | try { 33 | if (await isPending(issuesPromise)) { 34 | hooks.waiting.call(stats.compilation); 35 | config.logger.log(chalk.cyan('Type-checking in progress...')); 36 | } else { 37 | // wait 10ms to log issues after webpack stats 38 | await wait(10); 39 | } 40 | 41 | issues = await issuesPromise; 42 | } catch (error) { 43 | hooks.error.call(error, stats.compilation); 44 | return; 45 | } 46 | 47 | if ( 48 | !issues || // some error has been thrown 49 | state.issuesPromise !== issuesPromise // we have a new request - don't show results for the old one 50 | ) { 51 | return; 52 | } 53 | 54 | debug(`Got ${issues?.length || 0} issues from getIssuesWorker.`); 55 | 56 | // filter list of issues by provided issue predicate 57 | issues = issues.filter(config.issue.predicate); 58 | 59 | // modify list of issues in the plugin hooks 60 | issues = hooks.issues.call(issues, stats.compilation); 61 | 62 | const formatter = createWebpackFormatter(config.formatter.format, config.formatter.pathType); 63 | 64 | if (issues.length) { 65 | // follow webpack's approach - one process.write to stderr with all errors and warnings 66 | config.logger.error(issues.map((issue) => formatter(issue)).join('\n')); 67 | 68 | // print stats of the compilation 69 | config.logger.log(statsFormatter(issues, stats)); 70 | } else { 71 | config.logger.log(chalk.green('No typescript errors found.')); 72 | } 73 | 74 | // report issues to webpack-dev-server, if it's listening 75 | // skip reporting if there are no issues, to avoid an extra hot reload 76 | if (issues.length && state.webpackDevServerDoneTap) { 77 | issues.forEach((issue) => { 78 | const error = new IssueWebpackError( 79 | config.formatter.format(issue), 80 | config.formatter.pathType, 81 | issue 82 | ); 83 | 84 | if (issue.severity === 'warning') { 85 | stats.compilation.warnings.push(error); 86 | } else { 87 | stats.compilation.errors.push(error); 88 | } 89 | }); 90 | 91 | debug('Sending issues to the webpack-dev-server.'); 92 | state.webpackDevServerDoneTap.fn(stats); 93 | } 94 | }); 95 | } 96 | 97 | export { tapDoneToAsyncGetIssues }; 98 | -------------------------------------------------------------------------------- /src/hooks/tap-error-to-log-message.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import type * as webpack from 'webpack'; 3 | 4 | import type { ForkTsCheckerWebpackPluginConfig } from '../plugin-config'; 5 | import { getPluginHooks } from '../plugin-hooks'; 6 | import { RpcExitError } from '../rpc'; 7 | import { AbortError } from '../utils/async/abort-error'; 8 | 9 | function tapErrorToLogMessage( 10 | compiler: webpack.Compiler, 11 | config: ForkTsCheckerWebpackPluginConfig 12 | ) { 13 | const hooks = getPluginHooks(compiler); 14 | 15 | hooks.error.tap('ForkTsCheckerWebpackPlugin', (error) => { 16 | if (error instanceof AbortError) { 17 | return; 18 | } 19 | 20 | config.logger.error(String(error)); 21 | 22 | if (error instanceof RpcExitError) { 23 | if (error.signal === 'SIGINT') { 24 | config.logger.error( 25 | chalk.red( 26 | 'Issues checking service interrupted - If running in a docker container, this may be caused ' + 27 | "by the container running out of memory. If so, try increasing the container's memory limit " + 28 | 'or lowering the `memoryLimit` value in the ForkTsCheckerWebpackPlugin configuration.' 29 | ) 30 | ); 31 | } else { 32 | config.logger.error( 33 | chalk.red( 34 | 'Issues checking service aborted - probably out of memory. ' + 35 | 'Check the `memoryLimit` option in the ForkTsCheckerWebpackPlugin configuration.\n' + 36 | "If increasing the memory doesn't solve the issue, it's most probably a bug in the TypeScript." 37 | ) 38 | ); 39 | } 40 | } 41 | }); 42 | } 43 | 44 | export { tapErrorToLogMessage }; 45 | -------------------------------------------------------------------------------- /src/hooks/tap-stop-to-terminate-workers.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | import { getInfrastructureLogger } from '../infrastructure-logger'; 4 | import type { ForkTsCheckerWebpackPluginState } from '../plugin-state'; 5 | import type { RpcWorker } from '../rpc'; 6 | 7 | function tapStopToTerminateWorkers( 8 | compiler: webpack.Compiler, 9 | getIssuesWorker: RpcWorker, 10 | getDependenciesWorker: RpcWorker, 11 | state: ForkTsCheckerWebpackPluginState 12 | ) { 13 | const { debug } = getInfrastructureLogger(compiler); 14 | 15 | const terminateWorkers = () => { 16 | debug('Compiler is going to close - terminating workers...'); 17 | getIssuesWorker.terminate(); 18 | getDependenciesWorker.terminate(); 19 | }; 20 | 21 | compiler.hooks.watchClose.tap('ForkTsCheckerWebpackPlugin', () => { 22 | terminateWorkers(); 23 | }); 24 | 25 | compiler.hooks.done.tap('ForkTsCheckerWebpackPlugin', () => { 26 | if (!state.watching) { 27 | terminateWorkers(); 28 | } 29 | }); 30 | 31 | compiler.hooks.failed.tap('ForkTsCheckerWebpackPlugin', () => { 32 | if (!state.watching) { 33 | terminateWorkers(); 34 | } 35 | }); 36 | } 37 | 38 | export { tapStopToTerminateWorkers }; 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ForkTsCheckerWebpackPlugin } from './plugin'; 2 | 3 | export = ForkTsCheckerWebpackPlugin; 4 | -------------------------------------------------------------------------------- /src/infrastructure-logger.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | export interface InfrastructureLogger { 4 | log(...args: unknown[]): void; 5 | debug(...args: unknown[]): void; 6 | error(...args: unknown[]): void; 7 | warn(...args: unknown[]): void; 8 | info(...args: unknown[]): void; 9 | } 10 | 11 | export function getInfrastructureLogger(compiler: webpack.Compiler): InfrastructureLogger { 12 | const logger = compiler.getInfrastructureLogger('ForkTsCheckerWebpackPlugin'); 13 | 14 | return { 15 | log: logger.log.bind(logger), 16 | debug: logger.debug.bind(logger), 17 | error: logger.error.bind(logger), 18 | warn: logger.warn.bind(logger), 19 | info: logger.info.bind(logger), 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/issue/index.ts: -------------------------------------------------------------------------------- 1 | export * from './issue'; 2 | export * from './issue-severity'; 3 | export * from './issue-location'; 4 | -------------------------------------------------------------------------------- /src/issue/issue-config.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | import { createIssuePredicateFromIssueMatch } from './issue-match'; 4 | import type { IssuePredicateOption, IssueOptions } from './issue-options'; 5 | import type { IssuePredicate } from './issue-predicate'; 6 | import { composeIssuePredicates, createTrivialIssuePredicate } from './issue-predicate'; 7 | 8 | interface IssueConfig { 9 | predicate: IssuePredicate; 10 | } 11 | 12 | function createIssuePredicateFromOption( 13 | context: string, 14 | option: IssuePredicateOption 15 | ): IssuePredicate { 16 | if (Array.isArray(option)) { 17 | return composeIssuePredicates( 18 | option.map((option) => 19 | typeof option === 'function' ? option : createIssuePredicateFromIssueMatch(context, option) 20 | ) 21 | ); 22 | } 23 | 24 | return typeof option === 'function' 25 | ? option 26 | : createIssuePredicateFromIssueMatch(context, option); 27 | } 28 | 29 | function createIssueConfig( 30 | compiler: webpack.Compiler, 31 | options: IssueOptions | undefined 32 | ): IssueConfig { 33 | const context = compiler.options.context || process.cwd(); 34 | 35 | if (!options) { 36 | options = {} as IssueOptions; 37 | } 38 | 39 | const include = options.include 40 | ? createIssuePredicateFromOption(context, options.include) 41 | : createTrivialIssuePredicate(true); 42 | const exclude = options.exclude 43 | ? createIssuePredicateFromOption(context, options.exclude) 44 | : createTrivialIssuePredicate(false); 45 | 46 | return { 47 | predicate: (issue) => include(issue) && !exclude(issue), 48 | }; 49 | } 50 | 51 | export { IssueConfig, createIssueConfig }; 52 | -------------------------------------------------------------------------------- /src/issue/issue-location.ts: -------------------------------------------------------------------------------- 1 | import type { IssuePosition } from './issue-position'; 2 | import { compareIssuePositions } from './issue-position'; 3 | 4 | interface IssueLocation { 5 | start: IssuePosition; 6 | end: IssuePosition; 7 | } 8 | 9 | function compareIssueLocations(locationA?: IssueLocation, locationB?: IssueLocation) { 10 | if (locationA === locationB) { 11 | return 0; 12 | } 13 | 14 | if (!locationA) { 15 | return -1; 16 | } 17 | 18 | if (!locationB) { 19 | return 1; 20 | } 21 | 22 | return ( 23 | compareIssuePositions(locationA.start, locationB.start) || 24 | compareIssuePositions(locationA.end, locationB.end) 25 | ); 26 | } 27 | 28 | function formatIssueLocation(location: IssueLocation) { 29 | return `${location.start.line}:${location.start.column}`; 30 | } 31 | 32 | export { IssueLocation, compareIssueLocations, formatIssueLocation }; 33 | -------------------------------------------------------------------------------- /src/issue/issue-match.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import minimatch from 'minimatch'; 4 | 5 | import { forwardSlash } from '../utils/path/forward-slash'; 6 | 7 | import type { IssuePredicate } from './issue-predicate'; 8 | 9 | import type { Issue } from './index'; 10 | 11 | type IssueMatch = Partial>; 12 | 13 | function createIssuePredicateFromIssueMatch(context: string, match: IssueMatch): IssuePredicate { 14 | return (issue) => { 15 | const matchesSeverity = !match.severity || match.severity === issue.severity; 16 | const matchesCode = !match.code || match.code === issue.code; 17 | const matchesFile = 18 | !issue.file || 19 | (!!issue.file && 20 | (!match.file || minimatch(forwardSlash(path.relative(context, issue.file)), match.file))); 21 | 22 | return matchesSeverity && matchesCode && matchesFile; 23 | }; 24 | } 25 | 26 | export { IssueMatch, createIssuePredicateFromIssueMatch }; 27 | -------------------------------------------------------------------------------- /src/issue/issue-options.ts: -------------------------------------------------------------------------------- 1 | import type { IssueMatch } from './issue-match'; 2 | import type { IssuePredicate } from './issue-predicate'; 3 | 4 | type IssuePredicateOption = IssuePredicate | IssueMatch | (IssuePredicate | IssueMatch)[]; 5 | 6 | interface IssueOptions { 7 | include?: IssuePredicateOption; 8 | exclude?: IssuePredicateOption; 9 | } 10 | 11 | export { IssueOptions, IssuePredicateOption }; 12 | -------------------------------------------------------------------------------- /src/issue/issue-position.ts: -------------------------------------------------------------------------------- 1 | interface IssuePosition { 2 | line: number; 3 | column: number; 4 | } 5 | 6 | function compareIssuePositions(positionA?: IssuePosition, positionB?: IssuePosition) { 7 | if (positionA === positionB) { 8 | return 0; 9 | } 10 | 11 | if (!positionA) { 12 | return -1; 13 | } 14 | 15 | if (!positionB) { 16 | return 1; 17 | } 18 | 19 | return ( 20 | Math.sign(positionA.line - positionB.line) || Math.sign(positionA.column - positionB.column) 21 | ); 22 | } 23 | 24 | export { IssuePosition, compareIssuePositions }; 25 | -------------------------------------------------------------------------------- /src/issue/issue-predicate.ts: -------------------------------------------------------------------------------- 1 | import type { Issue } from './index'; 2 | 3 | type IssuePredicate = (issue: Issue) => boolean; 4 | 5 | function createTrivialIssuePredicate(result: boolean): IssuePredicate { 6 | return () => result; 7 | } 8 | 9 | function composeIssuePredicates(predicates: IssuePredicate[]): IssuePredicate { 10 | return (issue) => predicates.some((predicate) => predicate(issue)); 11 | } 12 | 13 | export { IssuePredicate, createTrivialIssuePredicate, composeIssuePredicates }; 14 | -------------------------------------------------------------------------------- /src/issue/issue-severity.ts: -------------------------------------------------------------------------------- 1 | type IssueSeverity = 'error' | 'warning'; 2 | 3 | function isIssueSeverity(value: unknown): value is IssueSeverity { 4 | return ['error', 'warning'].includes(value as IssueSeverity); 5 | } 6 | 7 | function compareIssueSeverities(severityA: IssueSeverity, severityB: IssueSeverity) { 8 | const [priorityA, priorityB] = [severityA, severityB].map((severity) => 9 | ['warning' /* 0 */, 'error' /* 1 */].indexOf(severity) 10 | ); 11 | 12 | return Math.sign(priorityB - priorityA); 13 | } 14 | 15 | export { IssueSeverity, isIssueSeverity, compareIssueSeverities }; 16 | -------------------------------------------------------------------------------- /src/issue/issue-webpack-error.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import * as webpack from 'webpack'; 4 | 5 | import type { FormatterPathType } from '../formatter'; 6 | import { forwardSlash } from '../utils/path/forward-slash'; 7 | import { relativeToContext } from '../utils/path/relative-to-context'; 8 | 9 | import type { Issue } from './issue'; 10 | import { formatIssueLocation } from './issue-location'; 11 | 12 | class IssueWebpackError extends webpack.WebpackError { 13 | readonly hideStack = true; 14 | 15 | constructor(message: string, pathType: FormatterPathType, readonly issue: Issue) { 16 | super(message); 17 | 18 | // to display issue location using `loc` property, webpack requires `error.module` which 19 | // should be a NormalModule instance. 20 | // to avoid such a dependency, we do a workaround - error.file will contain formatted location instead 21 | if (issue.file) { 22 | this.file = 23 | pathType === 'absolute' 24 | ? forwardSlash(path.resolve(issue.file)) 25 | : relativeToContext(issue.file, process.cwd()); 26 | 27 | if (issue.location) { 28 | this.file += `:${formatIssueLocation(issue.location)}`; 29 | } 30 | } 31 | 32 | Error.captureStackTrace(this, this.constructor); 33 | } 34 | } 35 | 36 | export { IssueWebpackError }; 37 | -------------------------------------------------------------------------------- /src/issue/issue.ts: -------------------------------------------------------------------------------- 1 | import type { IssueLocation } from './issue-location'; 2 | import { compareIssueLocations } from './issue-location'; 3 | import type { IssueSeverity } from './issue-severity'; 4 | import { compareIssueSeverities, isIssueSeverity } from './issue-severity'; 5 | 6 | interface Issue { 7 | severity: IssueSeverity; 8 | code: string; 9 | message: string; 10 | file?: string; 11 | location?: IssueLocation; 12 | } 13 | 14 | function isIssue(value: unknown): value is Issue { 15 | return ( 16 | !!value && 17 | typeof value === 'object' && 18 | isIssueSeverity((value as Issue).severity) && 19 | !!(value as Issue).code && 20 | !!(value as Issue).message 21 | ); 22 | } 23 | 24 | function compareStrings(stringA?: string, stringB?: string) { 25 | if (stringA === stringB) { 26 | return 0; 27 | } 28 | 29 | if (stringA === undefined || stringA === null) { 30 | return -1; 31 | } 32 | if (stringB === undefined || stringB === null) { 33 | return 1; 34 | } 35 | 36 | return stringA.toString().localeCompare(stringB.toString()); 37 | } 38 | 39 | function compareIssues(issueA: Issue, issueB: Issue) { 40 | return ( 41 | compareIssueSeverities(issueA.severity, issueB.severity) || 42 | compareStrings(issueA.file, issueB.file) || 43 | compareIssueLocations(issueA.location, issueB.location) || 44 | compareStrings(issueA.code, issueB.code) || 45 | compareStrings(issueA.message, issueB.message) || 46 | 0 /* EqualTo */ 47 | ); 48 | } 49 | 50 | function equalsIssues(issueA: Issue, issueB: Issue) { 51 | return compareIssues(issueA, issueB) === 0; 52 | } 53 | 54 | function deduplicateAndSortIssues(issues: Issue[]) { 55 | const sortedIssues = issues.filter(isIssue).sort(compareIssues); 56 | 57 | return sortedIssues.filter( 58 | (issue, index) => index === 0 || !equalsIssues(issue, sortedIssues[index - 1]) 59 | ); 60 | } 61 | 62 | export { Issue, isIssue, deduplicateAndSortIssues }; 63 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | interface Logger { 2 | log: (message: string) => void; 3 | error: (message: string) => void; 4 | } 5 | 6 | export { Logger }; 7 | -------------------------------------------------------------------------------- /src/plugin-config.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | import type { FormatterConfig } from './formatter'; 4 | import { createFormatterConfig } from './formatter'; 5 | import { getInfrastructureLogger } from './infrastructure-logger'; 6 | import type { IssueConfig } from './issue/issue-config'; 7 | import { createIssueConfig } from './issue/issue-config'; 8 | import type { Logger } from './logger'; 9 | import type { ForkTsCheckerWebpackPluginOptions } from './plugin-options'; 10 | import type { TypeScriptWorkerConfig } from './typescript/type-script-worker-config'; 11 | import { createTypeScriptWorkerConfig } from './typescript/type-script-worker-config'; 12 | 13 | interface ForkTsCheckerWebpackPluginConfig { 14 | async: boolean; 15 | typescript: TypeScriptWorkerConfig; 16 | issue: IssueConfig; 17 | formatter: FormatterConfig; 18 | logger: Logger; 19 | devServer: boolean; 20 | } 21 | 22 | function createPluginConfig( 23 | compiler: webpack.Compiler, 24 | options: ForkTsCheckerWebpackPluginOptions = {} 25 | ): ForkTsCheckerWebpackPluginConfig { 26 | return { 27 | async: options.async === undefined ? compiler.options.mode === 'development' : options.async, 28 | typescript: createTypeScriptWorkerConfig(compiler, options.typescript), 29 | issue: createIssueConfig(compiler, options.issue), 30 | formatter: createFormatterConfig(options.formatter), 31 | logger: 32 | options.logger === 'webpack-infrastructure' 33 | ? (() => { 34 | const { info, error } = getInfrastructureLogger(compiler); 35 | 36 | return { 37 | log: info, 38 | error, 39 | }; 40 | })() 41 | : options.logger || console, 42 | devServer: options.devServer !== false, 43 | }; 44 | } 45 | 46 | export { ForkTsCheckerWebpackPluginConfig, createPluginConfig }; 47 | -------------------------------------------------------------------------------- /src/plugin-hooks.ts: -------------------------------------------------------------------------------- 1 | import { SyncHook, SyncWaterfallHook, AsyncSeriesWaterfallHook } from 'tapable'; 2 | import type * as webpack from 'webpack'; 3 | 4 | import type { FilesChange } from './files-change'; 5 | import type { Issue } from './issue'; 6 | 7 | const compilerHookMap = new WeakMap< 8 | webpack.Compiler | webpack.MultiCompiler, 9 | ForkTsCheckerWebpackPluginHooks 10 | >(); 11 | 12 | function createPluginHooks() { 13 | return { 14 | start: new AsyncSeriesWaterfallHook<[FilesChange, webpack.Compilation]>([ 15 | 'change', 16 | 'compilation', 17 | ]), 18 | waiting: new SyncHook<[webpack.Compilation]>(['compilation']), 19 | canceled: new SyncHook<[webpack.Compilation]>(['compilation']), 20 | error: new SyncHook<[unknown, webpack.Compilation]>(['error', 'compilation']), 21 | issues: new SyncWaterfallHook<[Issue[], webpack.Compilation | undefined], void>([ 22 | 'issues', 23 | 'compilation', 24 | ]), 25 | }; 26 | } 27 | 28 | type ForkTsCheckerWebpackPluginHooks = ReturnType; 29 | 30 | function forwardPluginHooks( 31 | source: ForkTsCheckerWebpackPluginHooks, 32 | target: ForkTsCheckerWebpackPluginHooks 33 | ) { 34 | source.start.tapPromise('ForkTsCheckerWebpackPlugin', target.start.promise); 35 | source.waiting.tap('ForkTsCheckerWebpackPlugin', target.waiting.call); 36 | source.canceled.tap('ForkTsCheckerWebpackPlugin', target.canceled.call); 37 | source.error.tap('ForkTsCheckerWebpackPlugin', target.error.call); 38 | source.issues.tap('ForkTsCheckerWebpackPlugin', target.issues.call); 39 | } 40 | 41 | function getPluginHooks(compiler: webpack.Compiler | webpack.MultiCompiler) { 42 | let hooks = compilerHookMap.get(compiler); 43 | if (hooks === undefined) { 44 | hooks = createPluginHooks(); 45 | compilerHookMap.set(compiler, hooks); 46 | 47 | // proxy hooks for multi-compiler 48 | if ('compilers' in compiler) { 49 | compiler.compilers.forEach((childCompiler) => { 50 | const childHooks = getPluginHooks(childCompiler); 51 | 52 | if (hooks) { 53 | forwardPluginHooks(childHooks, hooks); 54 | } 55 | }); 56 | } 57 | } 58 | return hooks; 59 | } 60 | 61 | export { getPluginHooks, ForkTsCheckerWebpackPluginHooks }; 62 | -------------------------------------------------------------------------------- /src/plugin-options.ts: -------------------------------------------------------------------------------- 1 | import type { FormatterOptions } from './formatter'; 2 | import type { IssueOptions } from './issue/issue-options'; 3 | import type { Logger } from './logger'; 4 | import type { TypeScriptWorkerOptions } from './typescript/type-script-worker-options'; 5 | 6 | interface ForkTsCheckerWebpackPluginOptions { 7 | async?: boolean; 8 | typescript?: TypeScriptWorkerOptions; 9 | formatter?: FormatterOptions; 10 | issue?: IssueOptions; 11 | logger?: Logger | 'webpack-infrastructure'; 12 | devServer?: boolean; 13 | } 14 | 15 | export { ForkTsCheckerWebpackPluginOptions }; 16 | -------------------------------------------------------------------------------- /src/plugin-pools.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | 3 | import type { Pool } from './utils/async/pool'; 4 | import { createPool } from './utils/async/pool'; 5 | 6 | const issuesPool: Pool = createPool(Math.max(1, os.cpus().length)); 7 | const dependenciesPool: Pool = createPool(Math.max(1, os.cpus().length)); 8 | 9 | export { issuesPool, dependenciesPool }; 10 | -------------------------------------------------------------------------------- /src/plugin-state.ts: -------------------------------------------------------------------------------- 1 | import type { AbortController } from 'node-abort-controller'; 2 | import type { FullTap } from 'tapable'; 3 | 4 | import type { FilesChange } from './files-change'; 5 | import type { FilesMatch } from './files-match'; 6 | import type { Issue } from './issue'; 7 | 8 | interface ForkTsCheckerWebpackPluginState { 9 | issuesPromise: Promise; 10 | dependenciesPromise: Promise; 11 | abortController: AbortController | undefined; 12 | aggregatedFilesChange: FilesChange | undefined; 13 | lastDependencies: FilesMatch | undefined; 14 | watching: boolean; 15 | initialized: boolean; 16 | iteration: number; 17 | webpackDevServerDoneTap: FullTap | undefined; 18 | } 19 | 20 | function createPluginState(): ForkTsCheckerWebpackPluginState { 21 | return { 22 | issuesPromise: Promise.resolve(undefined), 23 | dependenciesPromise: Promise.resolve(undefined), 24 | abortController: undefined, 25 | aggregatedFilesChange: undefined, 26 | lastDependencies: undefined, 27 | watching: false, 28 | initialized: false, 29 | iteration: 0, 30 | webpackDevServerDoneTap: undefined, 31 | }; 32 | } 33 | 34 | export { ForkTsCheckerWebpackPluginState, createPluginState }; 35 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { cosmiconfigSync } from 'cosmiconfig'; 4 | import merge from 'deepmerge'; 5 | import type { JSONSchema7 } from 'json-schema'; 6 | import { validate } from 'schema-utils'; 7 | import type * as webpack from 'webpack'; 8 | 9 | import { tapAfterCompileToAddDependencies } from './hooks/tap-after-compile-to-add-dependencies'; 10 | import { tapAfterEnvironmentToPatchWatching } from './hooks/tap-after-environment-to-patch-watching'; 11 | import { tapErrorToLogMessage } from './hooks/tap-error-to-log-message'; 12 | import { tapStartToRunWorkers } from './hooks/tap-start-to-run-workers'; 13 | import { tapStopToTerminateWorkers } from './hooks/tap-stop-to-terminate-workers'; 14 | import { createPluginConfig } from './plugin-config'; 15 | import { getPluginHooks } from './plugin-hooks'; 16 | import type { ForkTsCheckerWebpackPluginOptions } from './plugin-options'; 17 | import schema from './plugin-options.json'; 18 | import { dependenciesPool, issuesPool } from './plugin-pools'; 19 | import { createPluginState } from './plugin-state'; 20 | import { createRpcWorker } from './rpc'; 21 | import { assertTypeScriptSupport } from './typescript/type-script-support'; 22 | import type { GetDependenciesWorker } from './typescript/worker/get-dependencies-worker'; 23 | import type { GetIssuesWorker } from './typescript/worker/get-issues-worker'; 24 | 25 | class ForkTsCheckerWebpackPlugin { 26 | /** 27 | * Current version of the plugin 28 | */ 29 | static readonly version: string = '{{VERSION}}'; // will be replaced by the @semantic-release/exec 30 | /** 31 | * Default pools for the plugin concurrency limit 32 | */ 33 | static readonly issuesPool = issuesPool; 34 | static readonly dependenciesPool = dependenciesPool; 35 | 36 | /** 37 | * @deprecated Use ForkTsCheckerWebpackPlugin.issuesPool instead 38 | */ 39 | static readonly pool = issuesPool; 40 | 41 | private readonly options: ForkTsCheckerWebpackPluginOptions; 42 | 43 | constructor(options: ForkTsCheckerWebpackPluginOptions = {}) { 44 | const explorerSync = cosmiconfigSync('fork-ts-checker'); 45 | const { config: externalOptions } = explorerSync.search() || {}; 46 | 47 | // first validate options directly passed to the constructor 48 | const config = { name: 'ForkTsCheckerWebpackPlugin' }; 49 | validate(schema as JSONSchema7, options, config); 50 | 51 | this.options = merge(externalOptions || {}, options || {}); 52 | 53 | // then validate merged options 54 | validate(schema as JSONSchema7, this.options, config); 55 | } 56 | 57 | public static getCompilerHooks(compiler: webpack.Compiler) { 58 | return getPluginHooks(compiler); 59 | } 60 | 61 | apply(compiler: webpack.Compiler) { 62 | const config = createPluginConfig(compiler, this.options); 63 | const state = createPluginState(); 64 | 65 | assertTypeScriptSupport(config.typescript); 66 | const getIssuesWorker = createRpcWorker( 67 | path.resolve(__dirname, './typescript/worker/get-issues-worker.js'), 68 | config.typescript, 69 | config.typescript.memoryLimit 70 | ); 71 | const getDependenciesWorker = createRpcWorker( 72 | path.resolve(__dirname, './typescript/worker/get-dependencies-worker.js'), 73 | config.typescript 74 | ); 75 | 76 | tapAfterEnvironmentToPatchWatching(compiler, state); 77 | tapStartToRunWorkers(compiler, getIssuesWorker, getDependenciesWorker, config, state); 78 | tapAfterCompileToAddDependencies(compiler, config, state); 79 | tapStopToTerminateWorkers(compiler, getIssuesWorker, getDependenciesWorker, state); 80 | tapErrorToLogMessage(compiler, config); 81 | } 82 | } 83 | 84 | export { ForkTsCheckerWebpackPlugin }; 85 | -------------------------------------------------------------------------------- /src/rpc/expose-rpc.ts: -------------------------------------------------------------------------------- 1 | import process from 'process'; 2 | 3 | import type { RpcMessage } from './types'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | export function exposeRpc(fn: (...args: any[]) => any) { 7 | const sendMessage = (message: RpcMessage) => 8 | new Promise((resolve, reject) => { 9 | if (!process.send) { 10 | reject(new Error(`Process ${process.pid} doesn't have IPC channels`)); 11 | } else if (!process.connected) { 12 | reject(new Error(`Process ${process.pid} doesn't have open IPC channels`)); 13 | } else { 14 | process.send(message, undefined, undefined, (error) => { 15 | if (error) { 16 | reject(error); 17 | } else { 18 | resolve(undefined); 19 | } 20 | }); 21 | } 22 | }); 23 | const handleMessage = async (message: RpcMessage) => { 24 | if (message.type === 'call') { 25 | if (!process.send) { 26 | // process disconnected - skip 27 | return; 28 | } 29 | 30 | let value: unknown; 31 | let error: unknown; 32 | try { 33 | value = await fn(...message.args); 34 | } catch (fnError) { 35 | error = fnError; 36 | } 37 | 38 | try { 39 | if (error) { 40 | await sendMessage({ 41 | type: 'reject', 42 | id: message.id, 43 | error, 44 | }); 45 | } else { 46 | await sendMessage({ 47 | type: 'resolve', 48 | id: message.id, 49 | value, 50 | }); 51 | } 52 | } catch (sendError) { 53 | // we can't send things back to the parent process - let's use stdout to communicate error 54 | if (error) { 55 | console.error(error); 56 | } 57 | console.error(sendError); 58 | } 59 | } 60 | }; 61 | process.on('message', handleMessage); 62 | } 63 | -------------------------------------------------------------------------------- /src/rpc/index.ts: -------------------------------------------------------------------------------- 1 | export { exposeRpc } from './expose-rpc'; 2 | export { wrapRpc } from './wrap-rpc'; 3 | export { createRpcWorker, getRpcWorkerData, RpcWorker } from './rpc-worker'; 4 | export { RpcExitError } from './rpc-error'; 5 | export { RpcRemoteMethod } from './types'; 6 | -------------------------------------------------------------------------------- /src/rpc/rpc-error.ts: -------------------------------------------------------------------------------- 1 | class RpcExitError extends Error { 2 | constructor( 3 | message: string, 4 | readonly code?: string | number | null, 5 | readonly signal?: string | null 6 | ) { 7 | super(message); 8 | this.name = 'RpcExitError'; 9 | } 10 | } 11 | 12 | export { RpcExitError }; 13 | -------------------------------------------------------------------------------- /src/rpc/rpc-worker.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import type { ChildProcess, ForkOptions } from 'child_process'; 3 | import * as process from 'process'; 4 | 5 | import type { RpcMethod, RpcRemoteMethod } from './types'; 6 | import { wrapRpc } from './wrap-rpc'; 7 | 8 | const WORKER_DATA_ENV_KEY = 'WORKER_DATA'; 9 | 10 | interface RpcWorkerBase { 11 | connect(): void; 12 | terminate(): void; 13 | readonly connected: boolean; 14 | readonly process: ChildProcess | undefined; 15 | } 16 | type RpcWorker = RpcWorkerBase & RpcRemoteMethod; 17 | 18 | function createRpcWorker( 19 | modulePath: string, 20 | data: unknown, 21 | memoryLimit?: number 22 | ): RpcWorker { 23 | const options: ForkOptions = { 24 | env: { 25 | ...process.env, 26 | [WORKER_DATA_ENV_KEY]: JSON.stringify(data || {}), 27 | }, 28 | stdio: ['inherit', 'inherit', 'inherit', 'ipc'], 29 | serialization: 'advanced', 30 | }; 31 | if (memoryLimit) { 32 | options.execArgv = [`--max-old-space-size=${memoryLimit}`]; 33 | } 34 | let childProcess: ChildProcess | undefined; 35 | let remoteMethod: RpcRemoteMethod | undefined; 36 | 37 | const worker: RpcWorkerBase = { 38 | connect() { 39 | if (childProcess && !childProcess.connected) { 40 | childProcess.kill('SIGTERM'); 41 | childProcess = undefined; 42 | remoteMethod = undefined; 43 | } 44 | if (!childProcess?.connected) { 45 | childProcess = child_process.fork(modulePath, options); 46 | remoteMethod = wrapRpc(childProcess); 47 | } 48 | }, 49 | terminate() { 50 | if (childProcess) { 51 | childProcess.kill('SIGTERM'); 52 | childProcess = undefined; 53 | remoteMethod = undefined; 54 | } 55 | }, 56 | get connected() { 57 | return Boolean(childProcess?.connected); 58 | }, 59 | get process() { 60 | return childProcess; 61 | }, 62 | }; 63 | 64 | return Object.assign((...args: unknown[]) => { 65 | if (!worker.connected) { 66 | // try to auto-connect 67 | worker.connect(); 68 | } 69 | 70 | if (!remoteMethod) { 71 | return Promise.reject('Worker is not connected - cannot perform RPC.'); 72 | } 73 | 74 | return remoteMethod(...args); 75 | }, worker) as RpcWorker; 76 | } 77 | function getRpcWorkerData(): unknown { 78 | return JSON.parse(process.env[WORKER_DATA_ENV_KEY] || '{}'); 79 | } 80 | 81 | export { createRpcWorker, getRpcWorkerData, RpcWorker }; 82 | -------------------------------------------------------------------------------- /src/rpc/types.ts: -------------------------------------------------------------------------------- 1 | interface RpcCallMessage { 2 | type: 'call'; 3 | id: string; 4 | args: unknown[]; 5 | } 6 | interface RpcResolveMessage { 7 | type: 'resolve'; 8 | id: string; 9 | value: unknown; 10 | } 11 | interface RpcRejectMessage { 12 | type: 'reject'; 13 | id: string; 14 | error: unknown; 15 | } 16 | type RpcMessage = RpcCallMessage | RpcResolveMessage | RpcRejectMessage; 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | type RpcMethod = (...args: any[]) => any; 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | type RpcRemoteMethod = T extends (...args: infer A) => infer R 22 | ? // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | R extends Promise 24 | ? (...args: A) => R 25 | : (...args: A) => Promise 26 | : (...args: unknown[]) => Promise; 27 | 28 | export { 29 | RpcCallMessage, 30 | RpcResolveMessage, 31 | RpcRejectMessage, 32 | RpcMessage, 33 | RpcMethod, 34 | RpcRemoteMethod, 35 | }; 36 | -------------------------------------------------------------------------------- /src/rpc/wrap-rpc.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcess } from 'child_process'; 2 | 3 | import { createControlledPromise } from '../utils/async/controlled-promise'; 4 | 5 | import { RpcExitError } from './rpc-error'; 6 | import type { RpcRemoteMethod, RpcMessage } from './types'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | export function wrapRpc any>( 10 | childProcess: ChildProcess 11 | ): RpcRemoteMethod { 12 | return (async (...args: unknown[]): Promise => { 13 | if (!childProcess.send) { 14 | throw new Error(`Process ${childProcess.pid} doesn't have IPC channels`); 15 | } else if (!childProcess.connected) { 16 | throw new Error(`Process ${childProcess.pid} doesn't have open IPC channels`); 17 | } 18 | 19 | const id = uuid(); 20 | 21 | // create promises 22 | const { 23 | promise: resultPromise, 24 | resolve: resolveResult, 25 | reject: rejectResult, 26 | } = createControlledPromise(); 27 | const { 28 | promise: sendPromise, 29 | resolve: resolveSend, 30 | reject: rejectSend, 31 | } = createControlledPromise(); 32 | 33 | const handleMessage = (message: RpcMessage) => { 34 | if (message?.id === id) { 35 | if (message.type === 'resolve') { 36 | // assume the contract is respected 37 | resolveResult(message.value as T); 38 | removeHandlers(); 39 | } else if (message.type === 'reject') { 40 | rejectResult(message.error); 41 | removeHandlers(); 42 | } 43 | } 44 | }; 45 | const handleClose = (code: string | number | null, signal: string | null) => { 46 | rejectResult( 47 | new RpcExitError( 48 | code 49 | ? `Process ${childProcess.pid} exited with code ${code}` + 50 | (signal ? ` [${signal}]` : '') 51 | : `Process ${childProcess.pid} exited` + (signal ? ` [${signal}]` : ''), 52 | code, 53 | signal 54 | ) 55 | ); 56 | removeHandlers(); 57 | }; 58 | 59 | // to prevent event handler leaks 60 | const removeHandlers = () => { 61 | childProcess.off('message', handleMessage); 62 | childProcess.off('close', handleClose); 63 | }; 64 | 65 | // add event listeners 66 | childProcess.on('message', handleMessage); 67 | childProcess.on('close', handleClose); 68 | // send call message 69 | childProcess.send( 70 | { 71 | type: 'call', 72 | id, 73 | args, 74 | }, 75 | (error) => { 76 | if (error) { 77 | rejectSend(error); 78 | removeHandlers(); 79 | } else { 80 | resolveSend(undefined); 81 | } 82 | } 83 | ); 84 | 85 | return sendPromise.then(() => resultPromise); 86 | }) as RpcRemoteMethod; 87 | } 88 | 89 | function uuid(): string { 90 | return new Array(4) 91 | .fill(0) 92 | .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)) 93 | .join('-'); 94 | } 95 | -------------------------------------------------------------------------------- /src/typescript/type-script-config-overwrite.ts: -------------------------------------------------------------------------------- 1 | interface TypeScriptConfigOverwrite { 2 | extends?: string; 3 | // eslint-disable-next-line 4 | compilerOptions?: any; 5 | include?: string[]; 6 | exclude?: string[]; 7 | files?: string[]; 8 | references?: { path: string; prepend?: boolean }[]; 9 | } 10 | 11 | export { TypeScriptConfigOverwrite }; 12 | -------------------------------------------------------------------------------- /src/typescript/type-script-diagnostics-options.ts: -------------------------------------------------------------------------------- 1 | interface TypeScriptDiagnosticsOptions { 2 | syntactic: boolean; 3 | semantic: boolean; 4 | declaration: boolean; 5 | global: boolean; 6 | } 7 | 8 | export { TypeScriptDiagnosticsOptions }; 9 | -------------------------------------------------------------------------------- /src/typescript/type-script-support.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import fs from 'fs-extra'; 4 | import * as semver from 'semver'; 5 | 6 | import type { TypeScriptWorkerConfig } from './type-script-worker-config'; 7 | 8 | function assertTypeScriptSupport(config: TypeScriptWorkerConfig) { 9 | let typescriptVersion: string | undefined; 10 | 11 | try { 12 | // eslint-disable-next-line @typescript-eslint/no-var-requires 13 | typescriptVersion = require(config.typescriptPath).version; 14 | } catch (error) { 15 | // silent catch 16 | } 17 | 18 | if (!typescriptVersion) { 19 | throw new Error( 20 | 'When you use ForkTsCheckerWebpackPlugin with typescript reporter enabled, you must install `typescript` package.' 21 | ); 22 | } 23 | 24 | if (semver.lt(typescriptVersion, '3.6.0')) { 25 | throw new Error( 26 | [ 27 | `ForkTsCheckerWebpackPlugin cannot use the current typescript version of ${typescriptVersion}.`, 28 | 'The minimum required version is 3.6.0.', 29 | ].join(os.EOL) 30 | ); 31 | } 32 | if (config.build && semver.lt(typescriptVersion, '3.8.0')) { 33 | throw new Error( 34 | [ 35 | `ForkTsCheckerWebpackPlugin doesn't support build option for the current typescript version of ${typescriptVersion}.`, 36 | 'The minimum required version is 3.8.0.', 37 | ].join(os.EOL) 38 | ); 39 | } 40 | 41 | if (!fs.existsSync(config.configFile)) { 42 | throw new Error( 43 | [ 44 | `Cannot find the "${config.configFile}" file.`, 45 | `Please check webpack and ForkTsCheckerWebpackPlugin configuration.`, 46 | `Possible errors:`, 47 | ' - wrong `context` directory in webpack configuration (if `configFile` is not set or is a relative path in the fork plugin configuration)', 48 | ' - wrong `typescript.configFile` path in the plugin configuration (should be a relative or absolute path)', 49 | ].join(os.EOL) 50 | ); 51 | } 52 | } 53 | 54 | export { assertTypeScriptSupport }; 55 | -------------------------------------------------------------------------------- /src/typescript/type-script-worker-config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import type * as webpack from 'webpack'; 4 | 5 | import type { TypeScriptConfigOverwrite } from './type-script-config-overwrite'; 6 | import type { TypeScriptDiagnosticsOptions } from './type-script-diagnostics-options'; 7 | import type { TypeScriptWorkerOptions } from './type-script-worker-options'; 8 | 9 | interface TypeScriptWorkerConfig { 10 | memoryLimit: number; 11 | configFile: string; 12 | configOverwrite: TypeScriptConfigOverwrite; 13 | build: boolean; 14 | context: string; 15 | mode: 'readonly' | 'write-dts' | 'write-tsbuildinfo' | 'write-references'; 16 | diagnosticOptions: TypeScriptDiagnosticsOptions; 17 | profile: boolean; 18 | typescriptPath: string; 19 | } 20 | 21 | function createTypeScriptWorkerConfig( 22 | compiler: webpack.Compiler, 23 | options: TypeScriptWorkerOptions | undefined 24 | ): TypeScriptWorkerConfig { 25 | let configFile = options?.configFile || 'tsconfig.json'; 26 | 27 | // ensure that `configFile` is an absolute normalized path 28 | configFile = path.normalize( 29 | path.isAbsolute(configFile) 30 | ? configFile 31 | : path.resolve(compiler.options.context || process.cwd(), configFile) 32 | ); 33 | 34 | const optionsAsObject: Exclude = 35 | typeof options === 'object' ? options : {}; 36 | 37 | const typescriptPath = optionsAsObject.typescriptPath || require.resolve('typescript'); 38 | 39 | return { 40 | memoryLimit: 2048, 41 | build: false, 42 | mode: optionsAsObject.build ? 'write-tsbuildinfo' : 'readonly', 43 | profile: false, 44 | ...optionsAsObject, 45 | configFile: configFile, 46 | configOverwrite: optionsAsObject.configOverwrite || {}, 47 | context: optionsAsObject.context || path.dirname(configFile), 48 | diagnosticOptions: { 49 | syntactic: false, // by default they are reported by the loader 50 | semantic: true, 51 | declaration: false, 52 | global: false, 53 | ...(optionsAsObject.diagnosticOptions || {}), 54 | }, 55 | typescriptPath: typescriptPath, 56 | }; 57 | } 58 | 59 | export { createTypeScriptWorkerConfig, TypeScriptWorkerConfig }; 60 | -------------------------------------------------------------------------------- /src/typescript/type-script-worker-options.ts: -------------------------------------------------------------------------------- 1 | import type { TypeScriptConfigOverwrite } from './type-script-config-overwrite'; 2 | import type { TypeScriptDiagnosticsOptions } from './type-script-diagnostics-options'; 3 | 4 | type TypeScriptWorkerOptions = { 5 | memoryLimit?: number; 6 | configFile?: string; 7 | configOverwrite?: TypeScriptConfigOverwrite; 8 | context?: string; 9 | build?: boolean; 10 | mode?: 'readonly' | 'write-tsbuildinfo' | 'write-dts' | 'write-references'; 11 | diagnosticOptions?: Partial; 12 | profile?: boolean; 13 | typescriptPath?: string; 14 | }; 15 | 16 | export { TypeScriptWorkerOptions }; 17 | -------------------------------------------------------------------------------- /src/typescript/worker/get-dependencies-worker.ts: -------------------------------------------------------------------------------- 1 | import type { FilesChange } from '../../files-change'; 2 | import type { FilesMatch } from '../../files-match'; 3 | import { exposeRpc } from '../../rpc'; 4 | 5 | import { 6 | didConfigFileChanged, 7 | didDependenciesProbablyChanged, 8 | invalidateConfig, 9 | } from './lib/config'; 10 | import { getDependencies, invalidateDependencies } from './lib/dependencies'; 11 | import { system } from './lib/system'; 12 | 13 | const getDependenciesWorker = (change: FilesChange): FilesMatch => { 14 | system.invalidateCache(); 15 | 16 | if (didConfigFileChanged(change) || didDependenciesProbablyChanged(getDependencies(), change)) { 17 | invalidateConfig(); 18 | invalidateDependencies(); 19 | } 20 | 21 | return getDependencies(); 22 | }; 23 | 24 | exposeRpc(getDependenciesWorker); 25 | export type GetDependenciesWorker = typeof getDependenciesWorker; 26 | -------------------------------------------------------------------------------- /src/typescript/worker/get-issues-worker.ts: -------------------------------------------------------------------------------- 1 | import type { FilesChange } from '../../files-change'; 2 | import type { Issue } from '../../issue'; 3 | import { exposeRpc } from '../../rpc'; 4 | 5 | import { invalidateArtifacts, registerArtifacts } from './lib/artifacts'; 6 | import { 7 | didConfigFileChanged, 8 | didDependenciesProbablyChanged, 9 | didRootFilesChanged, 10 | getParseConfigIssues, 11 | invalidateConfig, 12 | } from './lib/config'; 13 | import { getDependencies, invalidateDependencies } from './lib/dependencies'; 14 | import { getIssues, invalidateDiagnostics } from './lib/diagnostics'; 15 | import { 16 | disablePerformanceIfNeeded, 17 | enablePerformanceIfNeeded, 18 | printPerformanceMeasuresIfNeeded, 19 | } from './lib/performance'; 20 | import { invalidateProgram, useProgram } from './lib/program/program'; 21 | import { invalidateSolutionBuilder, useSolutionBuilder } from './lib/program/solution-builder'; 22 | import { 23 | invalidateWatchProgram, 24 | invalidateWatchProgramRootFileNames, 25 | useWatchProgram, 26 | } from './lib/program/watch-program'; 27 | import { system } from './lib/system'; 28 | import { dumpTracingLegendIfNeeded } from './lib/tracing'; 29 | import { invalidateTsBuildInfo } from './lib/tsbuildinfo'; 30 | import { config } from './lib/worker-config'; 31 | 32 | const getIssuesWorker = async (change: FilesChange, watching: boolean): Promise => { 33 | system.invalidateCache(); 34 | 35 | if (didConfigFileChanged(change)) { 36 | invalidateConfig(); 37 | invalidateDependencies(); 38 | invalidateArtifacts(); 39 | invalidateDiagnostics(); 40 | 41 | invalidateProgram(true); 42 | invalidateWatchProgram(true); 43 | invalidateSolutionBuilder(true); 44 | 45 | invalidateTsBuildInfo(); 46 | } else if (didDependenciesProbablyChanged(getDependencies(), change)) { 47 | invalidateConfig(); 48 | invalidateDependencies(); 49 | invalidateArtifacts(); 50 | 51 | if (didRootFilesChanged()) { 52 | invalidateWatchProgramRootFileNames(); 53 | invalidateSolutionBuilder(); 54 | } 55 | } 56 | 57 | registerArtifacts(); 58 | enablePerformanceIfNeeded(); 59 | 60 | const parseConfigIssues = getParseConfigIssues(); 61 | if (parseConfigIssues.length) { 62 | // report config parse issues and exit 63 | return parseConfigIssues; 64 | } 65 | 66 | // use proper implementation based on the config 67 | if (config.build) { 68 | useSolutionBuilder(); 69 | } else if (watching) { 70 | useWatchProgram(); 71 | } else { 72 | useProgram(); 73 | } 74 | 75 | // simulate file system events 76 | change.changedFiles?.forEach((changedFile) => { 77 | system?.invokeFileChanged(changedFile); 78 | }); 79 | change.deletedFiles?.forEach((deletedFile) => { 80 | system?.invokeFileDeleted(deletedFile); 81 | }); 82 | 83 | // wait for all queued events to be processed 84 | await system.waitForQueued(); 85 | 86 | // retrieve all collected diagnostics as normalized issues 87 | const issues = getIssues(); 88 | 89 | dumpTracingLegendIfNeeded(); 90 | printPerformanceMeasuresIfNeeded(); 91 | disablePerformanceIfNeeded(); 92 | 93 | return issues; 94 | }; 95 | 96 | exposeRpc(getIssuesWorker); 97 | export type GetIssuesWorker = typeof getIssuesWorker; 98 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/artifacts.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import type * as ts from 'typescript'; 4 | 5 | import type { FilesMatch } from '../../../files-match'; 6 | 7 | import { getParsedConfig, parseConfig } from './config'; 8 | import { system } from './system'; 9 | import { getTsBuildInfoEmitPath } from './tsbuildinfo'; 10 | import { typescript } from './typescript'; 11 | import { config } from './worker-config'; 12 | 13 | let artifacts: FilesMatch | undefined; 14 | 15 | export function getArtifacts(force = false): FilesMatch { 16 | if (!artifacts || force) { 17 | const parsedConfig = getParsedConfig(); 18 | 19 | artifacts = getArtifactsWorker(parsedConfig, config.context); 20 | } 21 | 22 | return artifacts; 23 | } 24 | 25 | export function invalidateArtifacts() { 26 | artifacts = undefined; 27 | } 28 | 29 | export function registerArtifacts() { 30 | system.setArtifacts(getArtifacts()); 31 | } 32 | 33 | function getArtifactsWorker( 34 | parsedConfig: ts.ParsedCommandLine, 35 | configFileContext: string, 36 | processedConfigFiles: string[] = [] 37 | ): FilesMatch { 38 | const files = new Set(); 39 | const dirs = new Set(); 40 | if (parsedConfig.fileNames.length > 0) { 41 | if (parsedConfig.options.outFile) { 42 | files.add(path.resolve(configFileContext, parsedConfig.options.outFile)); 43 | } 44 | const tsBuildInfoPath = getTsBuildInfoEmitPath(parsedConfig.options); 45 | if (tsBuildInfoPath) { 46 | files.add(path.resolve(configFileContext, tsBuildInfoPath)); 47 | } 48 | 49 | if (parsedConfig.options.outDir) { 50 | dirs.add(path.resolve(configFileContext, parsedConfig.options.outDir)); 51 | } 52 | } 53 | 54 | for (const projectReference of parsedConfig.projectReferences || []) { 55 | const configFile = typescript.resolveProjectReferencePath(projectReference); 56 | if (processedConfigFiles.includes(configFile)) { 57 | // handle circular dependencies 58 | continue; 59 | } 60 | const parsedConfig = parseConfig(configFile, path.dirname(configFile)); 61 | const childArtifacts = getArtifactsWorker(parsedConfig, configFileContext, [ 62 | ...processedConfigFiles, 63 | configFile, 64 | ]); 65 | childArtifacts.files.forEach((file) => { 66 | files.add(file); 67 | }); 68 | childArtifacts.dirs.forEach((dir) => { 69 | dirs.add(dir); 70 | }); 71 | } 72 | 73 | const extensions = [ 74 | typescript.Extension.Dts, 75 | typescript.Extension.Js, 76 | typescript.Extension.TsBuildInfo, 77 | ]; 78 | 79 | return { 80 | files: Array.from(files).map((file) => path.normalize(file)), 81 | dirs: Array.from(dirs).map((dir) => path.normalize(dir)), 82 | excluded: [], 83 | extensions, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/dependencies.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import type * as ts from 'typescript'; 4 | 5 | import type { FilesMatch } from '../../../files-match'; 6 | 7 | import { getParsedConfig, parseConfig } from './config'; 8 | import { typescript } from './typescript'; 9 | import { config } from './worker-config'; 10 | 11 | let dependencies: FilesMatch | undefined; 12 | 13 | export function getDependencies(force = false): FilesMatch { 14 | if (!dependencies || force) { 15 | const parsedConfig = getParsedConfig(); 16 | 17 | dependencies = getDependenciesWorker(parsedConfig, config.context); 18 | } 19 | 20 | return dependencies; 21 | } 22 | 23 | export function invalidateDependencies() { 24 | dependencies = undefined; 25 | } 26 | 27 | function getDependenciesWorker( 28 | parsedConfig: ts.ParsedCommandLine, 29 | configFileContext: string, 30 | processedConfigFiles: string[] = [] 31 | ): FilesMatch { 32 | const files = new Set(parsedConfig.fileNames); 33 | const configFilePath = parsedConfig.options.configFilePath; 34 | if (typeof configFilePath === 'string') { 35 | files.add(configFilePath); 36 | } 37 | const dirs = new Set(Object.keys(parsedConfig.wildcardDirectories || {})); 38 | const excluded = new Set( 39 | (parsedConfig.raw?.exclude || []).map((filePath: string) => 40 | path.resolve(configFileContext, filePath) 41 | ) 42 | ); 43 | 44 | for (const projectReference of parsedConfig.projectReferences || []) { 45 | const childConfigFilePath = typescript.resolveProjectReferencePath(projectReference); 46 | const childConfigContext = path.dirname(childConfigFilePath); 47 | if (processedConfigFiles.includes(childConfigFilePath)) { 48 | // handle circular dependencies 49 | continue; 50 | } 51 | const childParsedConfig = parseConfig(childConfigFilePath, childConfigContext); 52 | const childDependencies = getDependenciesWorker(childParsedConfig, childConfigContext, [ 53 | ...processedConfigFiles, 54 | childConfigFilePath, 55 | ]); 56 | childDependencies.files.forEach((file) => { 57 | files.add(file); 58 | }); 59 | childDependencies.dirs.forEach((dir) => { 60 | dirs.add(dir); 61 | }); 62 | } 63 | 64 | const extensions = [ 65 | typescript.Extension.Ts, 66 | typescript.Extension.Tsx, 67 | typescript.Extension.Js, 68 | typescript.Extension.Jsx, 69 | typescript.Extension.TsBuildInfo, 70 | ]; 71 | 72 | return { 73 | files: Array.from(files).map((file) => path.normalize(file)), 74 | dirs: Array.from(dirs).map((dir) => path.normalize(dir)), 75 | excluded: Array.from(excluded).map((aPath) => path.normalize(aPath)), 76 | extensions: extensions, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | 3 | import type * as ts from 'typescript'; 4 | 5 | import type { Issue, IssueLocation } from '../../../issue'; 6 | import { deduplicateAndSortIssues } from '../../../issue'; 7 | 8 | import { typescript } from './typescript'; 9 | import { config } from './worker-config'; 10 | 11 | const diagnosticsPerConfigFile = new Map(); 12 | 13 | export function updateDiagnostics(configFile: string, diagnostics: ts.Diagnostic[]): void { 14 | diagnosticsPerConfigFile.set(configFile, diagnostics); 15 | } 16 | 17 | export function getIssues(): Issue[] { 18 | const allDiagnostics: ts.Diagnostic[] = []; 19 | 20 | diagnosticsPerConfigFile.forEach((diagnostics) => { 21 | allDiagnostics.push(...diagnostics); 22 | }); 23 | 24 | return createIssuesFromDiagnostics(allDiagnostics); 25 | } 26 | 27 | export function invalidateDiagnostics(): void { 28 | diagnosticsPerConfigFile.clear(); 29 | } 30 | 31 | export function getDiagnosticsOfProgram(program: ts.Program | ts.BuilderProgram): ts.Diagnostic[] { 32 | const programDiagnostics: ts.Diagnostic[] = []; 33 | try { 34 | if (config.diagnosticOptions.syntactic) { 35 | programDiagnostics.push(...program.getSyntacticDiagnostics()); 36 | } 37 | if (config.diagnosticOptions.global) { 38 | programDiagnostics.push(...program.getGlobalDiagnostics()); 39 | } 40 | if (config.diagnosticOptions.semantic) { 41 | programDiagnostics.push(...program.getSemanticDiagnostics()); 42 | } 43 | if (config.diagnosticOptions.declaration) { 44 | programDiagnostics.push(...program.getDeclarationDiagnostics()); 45 | } 46 | } catch (e) { 47 | if (e instanceof Error) { 48 | programDiagnostics.push({ 49 | code: 1, 50 | category: 1, 51 | messageText: `TSC compiler crashed: ${e.message} 52 | ${e.stack}`, 53 | file: undefined, 54 | start: undefined, 55 | length: undefined, 56 | }); 57 | } 58 | } 59 | return programDiagnostics; 60 | } 61 | 62 | function createIssueFromDiagnostic(diagnostic: ts.Diagnostic): Issue { 63 | let file: string | undefined; 64 | let location: IssueLocation | undefined; 65 | 66 | if (diagnostic.file) { 67 | file = diagnostic.file.fileName; 68 | 69 | if (diagnostic.start && diagnostic.length) { 70 | const { line: startLine, character: startCharacter } = 71 | diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 72 | const { line: endLine, character: endCharacter } = 73 | diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start + diagnostic.length); 74 | 75 | location = { 76 | start: { 77 | line: startLine + 1, 78 | column: startCharacter + 1, 79 | }, 80 | end: { 81 | line: endLine + 1, 82 | column: endCharacter + 1, 83 | }, 84 | }; 85 | } 86 | } 87 | 88 | return { 89 | code: 'TS' + String(diagnostic.code), 90 | // we don't handle Suggestion and Message diagnostics 91 | severity: diagnostic.category === 0 ? 'warning' : 'error', 92 | message: typescript.flattenDiagnosticMessageText(diagnostic.messageText, os.EOL), 93 | file, 94 | location, 95 | }; 96 | } 97 | 98 | export function createIssuesFromDiagnostics(diagnostics: ts.Diagnostic[]): Issue[] { 99 | return deduplicateAndSortIssues( 100 | diagnostics.map((diagnostic) => createIssueFromDiagnostic(diagnostic)) 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/emit.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { getParsedConfig } from './config'; 4 | import { config } from './worker-config'; 5 | 6 | export function emitDtsIfNeeded(program: ts.Program | ts.BuilderProgram) { 7 | const parsedConfig = getParsedConfig(); 8 | 9 | if (config.mode === 'write-dts' && parsedConfig.options.declaration) { 10 | // emit .d.ts files only 11 | program.emit(undefined, undefined, undefined, true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/file-system/file-system.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line node/no-unsupported-features/node-builtins 2 | import type { Dirent, Stats } from 'fs'; 3 | 4 | /** 5 | * Interface to abstract file system implementation details. 6 | */ 7 | export interface FileSystem { 8 | // read 9 | exists(path: string): boolean; 10 | readFile(path: string, encoding?: string): string | undefined; 11 | readDir(path: string): Dirent[]; 12 | readStats(path: string): Stats | undefined; 13 | realPath(path: string): string; 14 | normalizePath(path: string): string; 15 | 16 | // write 17 | writeFile(path: string, data: string): void; 18 | deleteFile(path: string): void; 19 | createDir(path: string): void; 20 | updateTimes(path: string, atime: Date, mtime: Date): void; 21 | 22 | // cache 23 | clearCache(): void; 24 | } 25 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/file-system/mem-file-system.ts: -------------------------------------------------------------------------------- 1 | import type { Dirent, Stats } from 'fs'; 2 | import { dirname } from 'path'; 3 | 4 | import { fs as mem } from 'memfs'; 5 | 6 | import type { FileSystem } from './file-system'; 7 | import { realFileSystem } from './real-file-system'; 8 | 9 | /** 10 | * It's an implementation of FileSystem interface which reads and writes to the in-memory file system. 11 | */ 12 | export const memFileSystem: FileSystem = { 13 | ...realFileSystem, 14 | exists(path: string) { 15 | return exists(realFileSystem.realPath(path)); 16 | }, 17 | readFile(path: string, encoding?: string) { 18 | return readFile(realFileSystem.realPath(path), encoding); 19 | }, 20 | readDir(path: string) { 21 | return readDir(realFileSystem.realPath(path)); 22 | }, 23 | readStats(path: string) { 24 | return readStats(realFileSystem.realPath(path)); 25 | }, 26 | writeFile(path: string, data: string) { 27 | writeFile(realFileSystem.realPath(path), data); 28 | }, 29 | deleteFile(path: string) { 30 | deleteFile(realFileSystem.realPath(path)); 31 | }, 32 | createDir(path: string) { 33 | createDir(realFileSystem.realPath(path)); 34 | }, 35 | updateTimes(path: string, atime: Date, mtime: Date) { 36 | updateTimes(realFileSystem.realPath(path), atime, mtime); 37 | }, 38 | clearCache() { 39 | realFileSystem.clearCache(); 40 | }, 41 | }; 42 | 43 | function exists(path: string): boolean { 44 | return mem.existsSync(realFileSystem.normalizePath(path)); 45 | } 46 | 47 | function readStats(path: string): Stats | undefined { 48 | return exists(path) ? mem.statSync(realFileSystem.normalizePath(path)) : undefined; 49 | } 50 | 51 | function readFile(path: string, encoding?: string): string | undefined { 52 | const stats = readStats(path); 53 | 54 | if (stats && stats.isFile()) { 55 | return mem 56 | .readFileSync(realFileSystem.normalizePath(path), { encoding: encoding as BufferEncoding }) 57 | .toString(); 58 | } 59 | } 60 | 61 | function readDir(path: string): Dirent[] { 62 | const stats = readStats(path); 63 | 64 | if (stats && stats.isDirectory()) { 65 | return mem.readdirSync(realFileSystem.normalizePath(path), { 66 | withFileTypes: true, 67 | }) as Dirent[]; 68 | } 69 | 70 | return []; 71 | } 72 | 73 | function createDir(path: string) { 74 | mem.mkdirSync(realFileSystem.normalizePath(path), { recursive: true }); 75 | } 76 | 77 | function writeFile(path: string, data: string) { 78 | if (!exists(dirname(path))) { 79 | createDir(dirname(path)); 80 | } 81 | 82 | mem.writeFileSync(realFileSystem.normalizePath(path), data); 83 | } 84 | 85 | function deleteFile(path: string) { 86 | if (exists(path)) { 87 | mem.unlinkSync(realFileSystem.normalizePath(path)); 88 | } 89 | } 90 | 91 | function updateTimes(path: string, atime: Date, mtime: Date) { 92 | if (exists(path)) { 93 | mem.utimesSync(realFileSystem.normalizePath(path), atime, mtime); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/file-system/passive-file-system.ts: -------------------------------------------------------------------------------- 1 | import type { FileSystem } from './file-system'; 2 | import { memFileSystem } from './mem-file-system'; 3 | import { realFileSystem } from './real-file-system'; 4 | 5 | /** 6 | * It's an implementation of FileSystem interface which reads from the real file system, but write to the in-memory file system. 7 | */ 8 | export const passiveFileSystem: FileSystem = { 9 | ...memFileSystem, 10 | exists(path: string) { 11 | return exists(realFileSystem.realPath(path)); 12 | }, 13 | readFile(path: string, encoding?: string) { 14 | return readFile(realFileSystem.realPath(path), encoding); 15 | }, 16 | readDir(path: string) { 17 | return readDir(realFileSystem.realPath(path)); 18 | }, 19 | readStats(path: string) { 20 | return readStats(realFileSystem.realPath(path)); 21 | }, 22 | realPath(path: string) { 23 | return realFileSystem.realPath(path); 24 | }, 25 | clearCache() { 26 | realFileSystem.clearCache(); 27 | }, 28 | }; 29 | 30 | function exists(path: string) { 31 | return realFileSystem.exists(path) || memFileSystem.exists(path); 32 | } 33 | 34 | function readFile(path: string, encoding?: string) { 35 | const fsStats = realFileSystem.readStats(path); 36 | const memStats = memFileSystem.readStats(path); 37 | 38 | if (fsStats && memStats) { 39 | return fsStats.mtimeMs > memStats.mtimeMs 40 | ? realFileSystem.readFile(path, encoding) 41 | : memFileSystem.readFile(path, encoding); 42 | } else if (fsStats) { 43 | return realFileSystem.readFile(path, encoding); 44 | } else if (memStats) { 45 | return memFileSystem.readFile(path, encoding); 46 | } 47 | } 48 | 49 | function readDir(path: string) { 50 | const fsDirents = realFileSystem.readDir(path); 51 | const memDirents = memFileSystem.readDir(path); 52 | 53 | // merge list of dirents from fs and mem 54 | return fsDirents 55 | .filter((fsDirent) => !memDirents.some((memDirent) => memDirent.name === fsDirent.name)) 56 | .concat(memDirents); 57 | } 58 | 59 | function readStats(path: string) { 60 | const fsStats = realFileSystem.readStats(path); 61 | const memStats = memFileSystem.readStats(path); 62 | 63 | if (fsStats && memStats) { 64 | return fsStats.mtimeMs > memStats.mtimeMs ? fsStats : memStats; 65 | } else if (fsStats) { 66 | return fsStats; 67 | } else if (memStats) { 68 | return memStats; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/host/compiler-host.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { system } from '../system'; 4 | import { typescript } from '../typescript'; 5 | 6 | export function createCompilerHost(parsedConfig: ts.ParsedCommandLine): ts.CompilerHost { 7 | const baseCompilerHost = typescript.createCompilerHost(parsedConfig.options); 8 | 9 | return { 10 | ...baseCompilerHost, 11 | fileExists: system.fileExists, 12 | readFile: system.readFile, 13 | directoryExists: system.directoryExists, 14 | getDirectories: system.getDirectories, 15 | realpath: system.realpath, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/host/watch-compiler-host.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { system } from '../system'; 4 | import { typescript } from '../typescript'; 5 | 6 | export function createWatchCompilerHost( 7 | parsedConfig: ts.ParsedCommandLine, 8 | createProgram?: ts.CreateProgram, 9 | reportDiagnostic?: ts.DiagnosticReporter, 10 | reportWatchStatus?: ts.WatchStatusReporter, 11 | afterProgramCreate?: (program: TProgram) => void 12 | ): ts.WatchCompilerHostOfFilesAndCompilerOptions { 13 | const baseWatchCompilerHost = typescript.createWatchCompilerHost( 14 | parsedConfig.fileNames, 15 | parsedConfig.options, 16 | system, 17 | createProgram, 18 | reportDiagnostic, 19 | reportWatchStatus, 20 | parsedConfig.projectReferences 21 | ); 22 | 23 | return { 24 | ...baseWatchCompilerHost, 25 | createProgram( 26 | rootNames: ReadonlyArray | undefined, 27 | options: ts.CompilerOptions | undefined, 28 | compilerHost?: ts.CompilerHost, 29 | oldProgram?: TProgram, 30 | configFileParsingDiagnostics?: ReadonlyArray, 31 | projectReferences?: ReadonlyArray | undefined 32 | ): TProgram { 33 | return baseWatchCompilerHost.createProgram( 34 | rootNames, 35 | options, 36 | compilerHost, 37 | oldProgram, 38 | configFileParsingDiagnostics, 39 | projectReferences 40 | ); 41 | }, 42 | afterProgramCreate(program) { 43 | if (afterProgramCreate) { 44 | afterProgramCreate(program); 45 | } 46 | }, 47 | onWatchStatusChange(): void { 48 | // do nothing 49 | }, 50 | watchFile: system.watchFile, 51 | watchDirectory: system.watchDirectory, 52 | setTimeout: system.setTimeout, 53 | clearTimeout: system.clearTimeout, 54 | fileExists: system.fileExists, 55 | readFile: system.readFile, 56 | directoryExists: system.directoryExists, 57 | getDirectories: system.getDirectories, 58 | realpath: system.realpath, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/host/watch-solution-builder-host.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { system } from '../system'; 4 | import { typescript } from '../typescript'; 5 | 6 | import { createWatchCompilerHost } from './watch-compiler-host'; 7 | 8 | export function createWatchSolutionBuilderHost( 9 | parsedConfig: ts.ParsedCommandLine, 10 | createProgram?: ts.CreateProgram, 11 | reportDiagnostic?: ts.DiagnosticReporter, 12 | reportWatchStatus?: ts.WatchStatusReporter, 13 | reportSolutionBuilderStatus?: (diagnostic: ts.Diagnostic) => void, 14 | afterProgramCreate?: (program: TProgram) => void, 15 | afterProgramEmitAndDiagnostics?: (program: TProgram) => void 16 | ): ts.SolutionBuilderWithWatchHost { 17 | const controlledWatchCompilerHost = createWatchCompilerHost( 18 | parsedConfig, 19 | createProgram, 20 | reportDiagnostic, 21 | reportWatchStatus, 22 | afterProgramCreate 23 | ); 24 | 25 | return { 26 | ...controlledWatchCompilerHost, 27 | reportDiagnostic(diagnostic: ts.Diagnostic): void { 28 | if (reportDiagnostic) { 29 | reportDiagnostic(diagnostic); 30 | } 31 | }, 32 | reportSolutionBuilderStatus(diagnostic: ts.Diagnostic): void { 33 | if (reportSolutionBuilderStatus) { 34 | reportSolutionBuilderStatus(diagnostic); 35 | } 36 | }, 37 | afterProgramEmitAndDiagnostics(program): void { 38 | if (afterProgramEmitAndDiagnostics) { 39 | afterProgramEmitAndDiagnostics(program); 40 | } 41 | }, 42 | createDirectory(path: string): void { 43 | system.createDirectory(path); 44 | }, 45 | writeFile(path: string, data: string): void { 46 | system.writeFile(path, data); 47 | }, 48 | getModifiedTime(fileName: string): Date | undefined { 49 | return system.getModifiedTime(fileName); 50 | }, 51 | setModifiedTime(fileName: string, date: Date): void { 52 | system.setModifiedTime(fileName, date); 53 | }, 54 | deleteFile(fileName: string): void { 55 | system.deleteFile(fileName); 56 | }, 57 | getParsedCommandLine(fileName: string): ts.ParsedCommandLine | undefined { 58 | return typescript.getParsedCommandLineOfConfigFile( 59 | fileName, 60 | { skipLibCheck: true }, 61 | { 62 | ...system, 63 | onUnRecoverableConfigFileDiagnostic: (diagnostic) => { 64 | if (reportDiagnostic) { 65 | reportDiagnostic(diagnostic); 66 | } 67 | }, 68 | } 69 | ); 70 | }, 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/performance.ts: -------------------------------------------------------------------------------- 1 | import { typescript } from './typescript'; 2 | import { config } from './worker-config'; 3 | 4 | interface TypeScriptPerformance { 5 | enable?(): void; 6 | disable?(): void; 7 | forEachMeasure?(callback: (measureName: string, duration: number) => void): void; 8 | } 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | const performance: TypeScriptPerformance | undefined = (typescript as any).performance; 12 | 13 | export function enablePerformanceIfNeeded() { 14 | if (config.profile) { 15 | performance?.enable?.(); 16 | } 17 | } 18 | 19 | export function disablePerformanceIfNeeded() { 20 | if (config.profile) { 21 | performance?.disable?.(); 22 | } 23 | } 24 | 25 | export function printPerformanceMeasuresIfNeeded() { 26 | if (config.profile) { 27 | const measures: Record = {}; 28 | performance?.forEachMeasure?.((measureName, duration) => { 29 | measures[measureName] = duration; 30 | }); 31 | console.table(measures); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/program/program.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { getConfigFilePathFromProgram, getParsedConfig } from '../config'; 4 | import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics'; 5 | import { emitDtsIfNeeded } from '../emit'; 6 | import { createCompilerHost } from '../host/compiler-host'; 7 | import { startTracingIfNeeded, stopTracingIfNeeded } from '../tracing'; 8 | import { typescript } from '../typescript'; 9 | 10 | let compilerHost: ts.CompilerHost | undefined; 11 | let program: ts.Program | undefined; 12 | 13 | export function useProgram() { 14 | const parsedConfig = getParsedConfig(); 15 | 16 | if (!compilerHost) { 17 | compilerHost = createCompilerHost(parsedConfig); 18 | } 19 | if (!program) { 20 | startTracingIfNeeded(parsedConfig.options); 21 | program = typescript.createProgram({ 22 | rootNames: parsedConfig.fileNames, 23 | options: parsedConfig.options, 24 | projectReferences: parsedConfig.projectReferences, 25 | host: compilerHost, 26 | }); 27 | } 28 | 29 | updateDiagnostics(getConfigFilePathFromProgram(program), getDiagnosticsOfProgram(program)); 30 | emitDtsIfNeeded(program); 31 | stopTracingIfNeeded(program); 32 | } 33 | 34 | export function invalidateProgram(withHost = false) { 35 | if (withHost) { 36 | compilerHost = undefined; 37 | } 38 | program = undefined; 39 | } 40 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/program/solution-builder.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { getConfigFilePathFromBuilderProgram, getParsedConfig } from '../config'; 4 | import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics'; 5 | import { createWatchSolutionBuilderHost } from '../host/watch-solution-builder-host'; 6 | import { startTracingIfNeeded, stopTracingIfNeeded } from '../tracing'; 7 | import { emitTsBuildInfoIfNeeded } from '../tsbuildinfo'; 8 | import { typescript } from '../typescript'; 9 | import { config } from '../worker-config'; 10 | 11 | let solutionBuilderHost: 12 | | ts.SolutionBuilderWithWatchHost 13 | | undefined; 14 | let solutionBuilder: ts.SolutionBuilder | undefined; 15 | 16 | export function useSolutionBuilder() { 17 | if (!solutionBuilderHost) { 18 | const parsedConfig = getParsedConfig(); 19 | 20 | solutionBuilderHost = createWatchSolutionBuilderHost( 21 | parsedConfig, 22 | ( 23 | rootNames, 24 | compilerOptions, 25 | host, 26 | oldProgram, 27 | configFileParsingDiagnostics, 28 | projectReferences 29 | ) => { 30 | if (compilerOptions) { 31 | startTracingIfNeeded(compilerOptions); 32 | } 33 | return typescript.createSemanticDiagnosticsBuilderProgram( 34 | rootNames, 35 | compilerOptions, 36 | host, 37 | oldProgram, 38 | configFileParsingDiagnostics, 39 | projectReferences 40 | ); 41 | }, 42 | undefined, 43 | undefined, 44 | undefined, 45 | undefined, 46 | (builderProgram) => { 47 | updateDiagnostics( 48 | getConfigFilePathFromBuilderProgram(builderProgram), 49 | getDiagnosticsOfProgram(builderProgram) 50 | ); 51 | emitTsBuildInfoIfNeeded(builderProgram); 52 | stopTracingIfNeeded(builderProgram); 53 | } 54 | ); 55 | } 56 | if (!solutionBuilder) { 57 | solutionBuilder = typescript.createSolutionBuilderWithWatch( 58 | solutionBuilderHost, 59 | [config.configFile], 60 | { watch: true } 61 | ); 62 | solutionBuilder.build(); 63 | } 64 | } 65 | 66 | export function invalidateSolutionBuilder(withHost = false) { 67 | if (withHost) { 68 | solutionBuilderHost = undefined; 69 | } 70 | solutionBuilder = undefined; 71 | } 72 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/program/watch-program.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { getConfigFilePathFromBuilderProgram, getParsedConfig } from '../config'; 4 | import { getDependencies } from '../dependencies'; 5 | import { updateDiagnostics, getDiagnosticsOfProgram } from '../diagnostics'; 6 | import { emitDtsIfNeeded } from '../emit'; 7 | import { createWatchCompilerHost } from '../host/watch-compiler-host'; 8 | import { startTracingIfNeeded, stopTracingIfNeeded } from '../tracing'; 9 | import { emitTsBuildInfoIfNeeded } from '../tsbuildinfo'; 10 | import { typescript } from '../typescript'; 11 | 12 | let watchCompilerHost: 13 | | ts.WatchCompilerHostOfFilesAndCompilerOptions 14 | | undefined; 15 | let watchProgram: 16 | | ts.WatchOfFilesAndCompilerOptions 17 | | undefined; 18 | let shouldUpdateRootFiles = false; 19 | 20 | export function useWatchProgram() { 21 | if (!watchCompilerHost) { 22 | const parsedConfig = getParsedConfig(); 23 | 24 | watchCompilerHost = createWatchCompilerHost( 25 | parsedConfig, 26 | ( 27 | rootNames, 28 | compilerOptions, 29 | host, 30 | oldProgram, 31 | configFileParsingDiagnostics, 32 | projectReferences 33 | ) => { 34 | if (compilerOptions) { 35 | startTracingIfNeeded(compilerOptions); 36 | } 37 | return typescript.createSemanticDiagnosticsBuilderProgram( 38 | rootNames, 39 | compilerOptions, 40 | host, 41 | oldProgram, 42 | configFileParsingDiagnostics, 43 | projectReferences 44 | ); 45 | }, 46 | undefined, 47 | undefined, 48 | (builderProgram) => { 49 | updateDiagnostics( 50 | getConfigFilePathFromBuilderProgram(builderProgram), 51 | getDiagnosticsOfProgram(builderProgram) 52 | ); 53 | emitDtsIfNeeded(builderProgram); 54 | emitTsBuildInfoIfNeeded(builderProgram); 55 | stopTracingIfNeeded(builderProgram); 56 | } 57 | ); 58 | watchProgram = undefined; 59 | } 60 | if (!watchProgram) { 61 | watchProgram = typescript.createWatchProgram(watchCompilerHost); 62 | } 63 | 64 | if (shouldUpdateRootFiles) { 65 | // we have to update root files manually as don't use config file as a program input 66 | watchProgram.updateRootFileNames(getDependencies().files); 67 | shouldUpdateRootFiles = false; 68 | } 69 | } 70 | 71 | export function invalidateWatchProgram(withHost = false) { 72 | if (withHost) { 73 | watchCompilerHost = undefined; 74 | } 75 | watchProgram = undefined; 76 | } 77 | 78 | export function invalidateWatchProgramRootFileNames() { 79 | shouldUpdateRootFiles = true; 80 | } 81 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/tracing.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { getConfigFilePathFromCompilerOptions } from './config'; 4 | import { typescript } from './typescript'; 5 | import { config } from './worker-config'; 6 | 7 | // these types are internal in TypeScript, so reproduce them here 8 | type TracingMode = 'project' | 'build' | 'server'; 9 | interface Tracing { 10 | startTracing?: (tracingMode: TracingMode, traceDir: string, configFilePath?: string) => void; 11 | 12 | tracing?: { 13 | stopTracing(): void; 14 | dumpLegend(): void; 15 | }; 16 | } 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | const traceableTypescript: Tracing = typescript as any; 20 | 21 | export function startTracingIfNeeded(compilerOptions: ts.CompilerOptions) { 22 | if ( 23 | typeof compilerOptions.generateTrace === 'string' && 24 | typeof traceableTypescript.startTracing === 'function' 25 | ) { 26 | traceableTypescript.startTracing( 27 | config.build ? 'build' : 'project', 28 | compilerOptions.generateTrace, 29 | getConfigFilePathFromCompilerOptions(compilerOptions) 30 | ); 31 | } 32 | } 33 | 34 | export function stopTracingIfNeeded(program: ts.Program | ts.BuilderProgram) { 35 | const compilerOptions = program.getCompilerOptions(); 36 | 37 | if ( 38 | typeof compilerOptions.generateTrace === 'string' && 39 | typeof traceableTypescript.tracing?.stopTracing === 'function' 40 | ) { 41 | traceableTypescript.tracing.stopTracing(); 42 | } 43 | } 44 | 45 | export function dumpTracingLegendIfNeeded() { 46 | traceableTypescript.tracing?.dumpLegend(); 47 | } 48 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/tsbuildinfo.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import type * as ts from 'typescript'; 4 | 5 | import { getParsedConfig } from './config'; 6 | import { system } from './system'; 7 | import { typescript } from './typescript'; 8 | import { config } from './worker-config'; 9 | 10 | export function invalidateTsBuildInfo() { 11 | const parsedConfig = getParsedConfig(); 12 | 13 | // try to remove outdated .tsbuildinfo file for incremental mode 14 | if ( 15 | typeof typescript.getTsBuildInfoEmitOutputFilePath === 'function' && 16 | config.mode !== 'readonly' && 17 | parsedConfig.options.incremental 18 | ) { 19 | const tsBuildInfoPath = typescript.getTsBuildInfoEmitOutputFilePath(parsedConfig.options); 20 | if (tsBuildInfoPath) { 21 | try { 22 | system.deleteFile(tsBuildInfoPath); 23 | } catch (error) { 24 | // silent 25 | } 26 | } 27 | } 28 | } 29 | 30 | export function emitTsBuildInfoIfNeeded(builderProgram: ts.BuilderProgram) { 31 | const parsedConfig = getParsedConfig(); 32 | 33 | if (config.mode !== 'readonly' && parsedConfig && isIncrementalEnabled(parsedConfig.options)) { 34 | const program = builderProgram.getProgram(); 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | if (typeof (program as any).emitBuildInfo === 'function') { 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | (program as any).emitBuildInfo(); 39 | } 40 | } 41 | } 42 | 43 | export function getTsBuildInfoEmitPath(compilerOptions: ts.CompilerOptions) { 44 | if (typeof typescript.getTsBuildInfoEmitOutputFilePath === 'function') { 45 | return typescript.getTsBuildInfoEmitOutputFilePath(compilerOptions); 46 | } 47 | 48 | const removeJsonExtension = (filePath: string) => 49 | filePath.endsWith('.json') ? filePath.slice(0, -'.json'.length) : filePath; 50 | 51 | // based on the implementation from typescript 52 | const configFile = compilerOptions.configFilePath as string; 53 | if (!isIncrementalEnabled(compilerOptions)) { 54 | return undefined; 55 | } 56 | if (compilerOptions.tsBuildInfoFile) { 57 | return compilerOptions.tsBuildInfoFile; 58 | } 59 | const outPath = compilerOptions.outFile || compilerOptions.out; 60 | let buildInfoExtensionLess; 61 | if (outPath) { 62 | buildInfoExtensionLess = removeJsonExtension(outPath); 63 | } else { 64 | if (!configFile) { 65 | return undefined; 66 | } 67 | const configFileExtensionLess = removeJsonExtension(configFile); 68 | buildInfoExtensionLess = compilerOptions.outDir 69 | ? compilerOptions.rootDir 70 | ? path.resolve( 71 | compilerOptions.outDir, 72 | path.relative(compilerOptions.rootDir, configFileExtensionLess) 73 | ) 74 | : path.resolve(compilerOptions.outDir, path.basename(configFileExtensionLess)) 75 | : configFileExtensionLess; 76 | } 77 | return buildInfoExtensionLess + '.tsbuildinfo'; 78 | } 79 | 80 | function isIncrementalEnabled(compilerOptions: ts.CompilerOptions) { 81 | return Boolean( 82 | (compilerOptions.incremental || compilerOptions.composite) && !compilerOptions.outFile 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/typescript.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | import { config } from './worker-config'; 4 | 5 | // eslint-disable-next-line 6 | export const typescript: typeof ts = require(config.typescriptPath); 7 | -------------------------------------------------------------------------------- /src/typescript/worker/lib/worker-config.ts: -------------------------------------------------------------------------------- 1 | import { getRpcWorkerData } from '../../../rpc'; 2 | import type { TypeScriptWorkerConfig } from '../../type-script-worker-config'; 3 | 4 | export const config = getRpcWorkerData() as TypeScriptWorkerConfig; 5 | -------------------------------------------------------------------------------- /src/utils/async/abort-error.ts: -------------------------------------------------------------------------------- 1 | import type { AbortSignal } from 'node-abort-controller'; 2 | 3 | class AbortError extends Error { 4 | constructor(message = 'Task aborted.') { 5 | super(message); 6 | this.name = 'AbortError'; 7 | } 8 | 9 | static throwIfAborted(signal: AbortSignal | undefined) { 10 | if (signal?.aborted) { 11 | throw new AbortError(); 12 | } 13 | } 14 | } 15 | 16 | export { AbortError }; 17 | -------------------------------------------------------------------------------- /src/utils/async/controlled-promise.ts: -------------------------------------------------------------------------------- 1 | function createControlledPromise() { 2 | let resolve: (value: T) => void = () => undefined; 3 | let reject: (error: unknown) => void = () => undefined; 4 | const promise = new Promise((aResolve, aReject) => { 5 | resolve = aResolve; 6 | reject = aReject; 7 | }); 8 | 9 | return { 10 | promise, 11 | resolve, 12 | reject, 13 | }; 14 | } 15 | 16 | export { createControlledPromise }; 17 | -------------------------------------------------------------------------------- /src/utils/async/is-pending.ts: -------------------------------------------------------------------------------- 1 | function isPending(promise: Promise, timeout = 100) { 2 | return Promise.race([ 3 | promise.then(() => false).catch(() => false), 4 | new Promise((resolve) => setTimeout(() => resolve(true), timeout)), 5 | ]); 6 | } 7 | 8 | export { isPending }; 9 | -------------------------------------------------------------------------------- /src/utils/async/pool.ts: -------------------------------------------------------------------------------- 1 | import type { AbortSignal } from 'node-abort-controller'; 2 | 3 | import { AbortError } from './abort-error'; 4 | 5 | type Task = (signal?: AbortSignal) => Promise; 6 | 7 | interface Pool { 8 | submit(task: Task, signal?: AbortSignal): Promise; 9 | size: number; 10 | readonly pending: number; 11 | readonly drained: Promise; 12 | } 13 | 14 | function createPool(size: number): Pool { 15 | let pendingPromises: Promise[] = []; 16 | 17 | const pool = { 18 | async submit(task: Task, signal?: AbortSignal): Promise { 19 | while (pendingPromises.length >= pool.size) { 20 | AbortError.throwIfAborted(signal); 21 | await Promise.race(pendingPromises).catch(() => undefined); 22 | } 23 | 24 | AbortError.throwIfAborted(signal); 25 | 26 | const taskPromise = task(signal).finally(() => { 27 | pendingPromises = pendingPromises.filter( 28 | (pendingPromise) => pendingPromise !== taskPromise 29 | ); 30 | }); 31 | pendingPromises.push(taskPromise); 32 | 33 | return taskPromise; 34 | }, 35 | size, 36 | get pending() { 37 | return pendingPromises.length; 38 | }, 39 | get drained() { 40 | // eslint-disable-next-line no-async-promise-executor 41 | return new Promise(async (resolve) => { 42 | while (pendingPromises.length > 0) { 43 | await Promise.race(pendingPromises).catch(() => undefined); 44 | } 45 | resolve(undefined); 46 | }); 47 | }, 48 | }; 49 | 50 | return pool; 51 | } 52 | 53 | export { Pool, createPool }; 54 | -------------------------------------------------------------------------------- /src/utils/async/wait.ts: -------------------------------------------------------------------------------- 1 | function wait(timeout: number) { 2 | return new Promise((resolve) => setTimeout(resolve, timeout)); 3 | } 4 | 5 | export { wait }; 6 | -------------------------------------------------------------------------------- /src/utils/path/forward-slash.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | /** 4 | * Replaces backslashes with one forward slash 5 | * @param input 6 | */ 7 | function forwardSlash(input: string): string { 8 | return path.normalize(input).replace(/\\+/g, '/'); 9 | } 10 | 11 | export { forwardSlash }; 12 | -------------------------------------------------------------------------------- /src/utils/path/is-inside-another-path.ts: -------------------------------------------------------------------------------- 1 | import { relative, isAbsolute } from 'path'; 2 | 3 | function isInsideAnotherPath(parent: string, directory: string): boolean { 4 | const relativePart = relative(parent, directory); 5 | // Tested folder is above parent. 6 | if (relativePart.startsWith('..')) { 7 | return false; 8 | } 9 | // Tested folder is the same as parent. 10 | if (relativePart.length === 0) { 11 | return false; 12 | } 13 | // Tested directory has nothing in common with parent. 14 | if (isAbsolute(relativePart)) { 15 | return false; 16 | } 17 | // Last option, must be subfolder. 18 | return true; 19 | } 20 | 21 | export { isInsideAnotherPath }; 22 | -------------------------------------------------------------------------------- /src/utils/path/relative-to-context.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { forwardSlash } from './forward-slash'; 4 | 5 | function relativeToContext(file: string, context: string) { 6 | let fileInContext = forwardSlash(path.relative(context, file)); 7 | if (!fileInContext.startsWith('../')) { 8 | fileInContext = './' + fileInContext; 9 | } 10 | 11 | return fileInContext; 12 | } 13 | 14 | export { relativeToContext }; 15 | -------------------------------------------------------------------------------- /src/watch/watch-file-system.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from 'events'; 2 | 3 | import type * as webpack from 'webpack'; 4 | 5 | // watchpack v1 and v2 internal interface 6 | interface Watchpack extends EventEmitter { 7 | _onChange(item: string, mtime: number, file: string, type?: string): void; 8 | _onRemove(item: string, file: string, type?: string): void; 9 | } 10 | 11 | type Watch = NonNullable['watch']; 12 | 13 | interface WatchFileSystem { 14 | watcher?: Watchpack; 15 | wfs?: { 16 | watcher: Watchpack; 17 | }; 18 | watch: Watch; 19 | } 20 | 21 | export { WatchFileSystem, Watchpack }; 22 | -------------------------------------------------------------------------------- /test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TypeStrong/fork-ts-checker-webpack-plugin/9f70a3dcdaf216177b9b7b85426fc8e473bfad4e/test/.DS_Store -------------------------------------------------------------------------------- /test/e2e/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TypeStrong/fork-ts-checker-webpack-plugin/9f70a3dcdaf216177b9b7b85426fc8e473bfad4e/test/e2e/.DS_Store -------------------------------------------------------------------------------- /test/e2e/driver/listener.ts: -------------------------------------------------------------------------------- 1 | interface Listener { 2 | resolve(value: TValue): void; 3 | reject(error: unknown): void; 4 | } 5 | 6 | interface QueuedListener extends Listener { 7 | apply(listener: Listener): void; 8 | resolved: TValue | undefined; 9 | rejected: unknown | undefined; 10 | status: 'pending' | 'resolved' | 'rejected'; 11 | } 12 | 13 | function createQueuedListener(): QueuedListener { 14 | let resolve: (value: TValue) => void | undefined; 15 | let reject: (error: unknown) => void | undefined; 16 | 17 | const queuedListener: QueuedListener = { 18 | resolve(value) { 19 | if (queuedListener.status === 'pending') { 20 | queuedListener.resolved = value; 21 | queuedListener.status = 'resolved'; 22 | 23 | if (resolve) { 24 | resolve(value); 25 | } 26 | } 27 | }, 28 | reject(error) { 29 | if (queuedListener.status === 'pending') { 30 | queuedListener.rejected = error; 31 | queuedListener.status = 'rejected'; 32 | 33 | if (reject) { 34 | reject(error); 35 | } 36 | } 37 | }, 38 | apply(listener) { 39 | switch (queuedListener.status) { 40 | case 'resolved': 41 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 42 | listener.resolve(queuedListener.resolved!); 43 | break; 44 | case 'rejected': 45 | listener.reject(queuedListener.rejected); 46 | break; 47 | case 'pending': 48 | resolve = listener.resolve; 49 | reject = listener.reject; 50 | break; 51 | } 52 | }, 53 | resolved: undefined, 54 | rejected: undefined, 55 | status: 'pending', 56 | }; 57 | 58 | return queuedListener; 59 | } 60 | 61 | export { Listener, QueuedListener, createQueuedListener }; 62 | -------------------------------------------------------------------------------- /test/e2e/driver/webpack-dev-server-driver.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcess } from 'child_process'; 2 | 3 | import stripAnsi from 'strip-ansi'; 4 | 5 | import type { Listener, QueuedListener } from './listener'; 6 | import { createQueuedListener } from './listener'; 7 | import { extractWebpackErrors } from './webpack-errors-extractor'; 8 | 9 | interface WebpackDevServerDriver { 10 | process: ChildProcess; 11 | waitForErrors: (timeout?: number) => Promise; 12 | waitForNoErrors: (timeout?: number) => Promise; 13 | } 14 | 15 | interface AsyncListener extends Listener { 16 | active: boolean; 17 | } 18 | interface QueuedAsyncListener extends AsyncListener, QueuedListener { 19 | apply(listener: AsyncListener): void; 20 | } 21 | 22 | function createQueuedAsyncListener(active = false): QueuedAsyncListener { 23 | const queuedListener = createQueuedListener(); 24 | const asyncListener: QueuedAsyncListener = { 25 | ...queuedListener, 26 | apply(listener) { 27 | queuedListener.apply(listener); 28 | asyncListener.active = listener.active; 29 | }, 30 | active, 31 | }; 32 | 33 | return asyncListener; 34 | } 35 | 36 | function createWebpackDevServerDriver( 37 | process: ChildProcess, 38 | async: boolean, 39 | defaultTimeout = 30000 40 | ): WebpackDevServerDriver { 41 | let errorsListener = createQueuedAsyncListener(); 42 | let noErrorsListener = createQueuedAsyncListener(); 43 | let errors: string[] = []; 44 | 45 | function nextIteration() { 46 | errorsListener = createQueuedAsyncListener(); 47 | noErrorsListener = createQueuedAsyncListener(); 48 | errors = []; 49 | } 50 | 51 | function activateListeners() { 52 | noErrorsListener.active = true; 53 | errorsListener.active = true; 54 | } 55 | 56 | if (process.stdout) { 57 | process.stdout.on('data', (data) => { 58 | const content = stripAnsi(data.toString()); 59 | 60 | if (async && content.includes('No typescript errors found.')) { 61 | noErrorsListener.resolve(); 62 | } 63 | 64 | if (content.includes('Compiled successfully.')) { 65 | if (!async) { 66 | noErrorsListener.resolve(); 67 | } else { 68 | activateListeners(); 69 | } 70 | } 71 | 72 | if (content.includes('Failed to compile.') || content.includes('Compiled with warnings.')) { 73 | if (!async) { 74 | errorsListener.resolve(errors); 75 | } else { 76 | activateListeners(); 77 | } 78 | } 79 | }); 80 | } 81 | 82 | if (process.stderr) { 83 | process.stderr.on('data', (data) => { 84 | const content = stripAnsi(data.toString()); 85 | const extracted = extractWebpackErrors(content); 86 | errors.push(...extracted); 87 | 88 | if (async && extracted.length && errorsListener.active) { 89 | errorsListener.resolve(extracted); 90 | } 91 | }); 92 | } 93 | 94 | return { 95 | process: process, 96 | waitForErrors: (timeout = defaultTimeout) => 97 | new Promise((resolve, reject) => { 98 | const timeoutId = setTimeout(() => { 99 | reject(new Error('Exceeded time on waiting for errors to appear.')); 100 | nextIteration(); 101 | }, timeout); 102 | 103 | errorsListener.apply({ 104 | resolve: (results) => { 105 | clearTimeout(timeoutId); 106 | nextIteration(); 107 | resolve(results); 108 | }, 109 | reject: (error) => { 110 | clearTimeout(timeoutId); 111 | nextIteration(); 112 | reject(error); 113 | }, 114 | active: !async, // for async, we need to activate listener manually 115 | }); 116 | }), 117 | waitForNoErrors: (timeout = defaultTimeout) => 118 | new Promise((resolve, reject) => { 119 | const timeoutId = setTimeout(() => { 120 | reject(new Error('Exceeded time on waiting for no errors message to appear.')); 121 | nextIteration(); 122 | }, timeout); 123 | 124 | noErrorsListener.apply({ 125 | resolve: () => { 126 | clearTimeout(timeoutId); 127 | nextIteration(); 128 | resolve(); 129 | }, 130 | reject: (error) => { 131 | clearTimeout(timeoutId); 132 | nextIteration(); 133 | reject(error); 134 | }, 135 | active: !async, // for async, we need to activate listener manually 136 | }); 137 | }), 138 | }; 139 | } 140 | 141 | export { WebpackDevServerDriver, createWebpackDevServerDriver }; 142 | -------------------------------------------------------------------------------- /test/e2e/driver/webpack-errors-extractor.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi'; 2 | 3 | function isLineRelatedToTsLoader(line: string) { 4 | return line.includes('[tsl]') || line.includes('ts-loader'); 5 | } 6 | 7 | function extractWebpackErrors(content: string): string[] { 8 | const lines = stripAnsi(content).split(/\r\n?|\n/); 9 | const errors: string[] = []; 10 | let currentError: string | undefined = undefined; 11 | 12 | for (const line of lines) { 13 | if (currentError) { 14 | if (line === '') { 15 | errors.push(currentError); 16 | currentError = undefined; 17 | } else { 18 | if (isLineRelatedToTsLoader(line)) { 19 | currentError = undefined; 20 | } else { 21 | currentError += '\n' + line; 22 | } 23 | } 24 | } else { 25 | if ( 26 | (line.startsWith('ERROR') || line.startsWith('WARNING')) && 27 | !isLineRelatedToTsLoader(line) 28 | ) { 29 | currentError = line; 30 | } 31 | } 32 | } 33 | 34 | return errors; 35 | } 36 | 37 | export { extractWebpackErrors }; 38 | -------------------------------------------------------------------------------- /test/e2e/fixtures/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TypeStrong/fork-ts-checker-webpack-plugin/9f70a3dcdaf216177b9b7b85426fc8e473bfad4e/test/e2e/fixtures/.DS_Store -------------------------------------------------------------------------------- /test/e2e/fixtures/type-definitions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-definitions-fixture", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@types/node": "^18.11.9", 8 | "typescript": "~5.8.0", 9 | "webpack": "^5.98.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/fixtures/type-definitions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "lib": ["ES6", "DOM"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": false, 9 | "skipDefaultLibCheck": false, 10 | "strict": true, 11 | "baseUrl": "./" 12 | }, 13 | "include": ["./"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /test/e2e/fixtures/type-definitions/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 2 | 3 | const config = { 4 | entry: './src/index.ts', 5 | output: { 6 | hashFunction: 'xxhash64', // @see https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported 7 | }, 8 | plugins: [ 9 | new ForkTsCheckerWebpackPlugin({ 10 | async: 'invalid_value', 11 | typescript: {}, 12 | }), 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-basic", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "ts-loader": "^5.0.0", 8 | "typescript": "^3.8.0", 9 | "webpack": "^5.54.0", 10 | "webpack-cli": "^4.0.0", 11 | "webpack-dev-server": "^3.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-basic/src/authenticate.ts: -------------------------------------------------------------------------------- 1 | import { User } from './model/User'; 2 | 3 | async function login(email: string, password: string): Promise { 4 | const response = await fetch('/login', { 5 | method: 'POST', 6 | body: JSON.stringify({ email, password }), 7 | }); 8 | return response.json(); 9 | } 10 | 11 | async function logout(): Promise { 12 | const response = await fetch('/logout', { 13 | method: 'POST', 14 | }); 15 | return response.json(); 16 | } 17 | 18 | export { login, logout }; 19 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-basic/src/index.ts: -------------------------------------------------------------------------------- 1 | import { login } from './authenticate'; 2 | import { getUserName } from './model/User'; 3 | 4 | const emailInput = document.getElementById('email'); 5 | const passwordInput = document.getElementById('password'); 6 | const loginForm = document.getElementById('login'); 7 | 8 | if (!emailInput) { 9 | throw new Error('Cannot find #email input.'); 10 | } 11 | if (!passwordInput) { 12 | throw new Error('Cannot find #password input.'); 13 | } 14 | if (!loginForm) { 15 | throw new Error('Cannot find #login form.'); 16 | } 17 | 18 | let email = ''; 19 | let password = ''; 20 | 21 | emailInput.addEventListener('change', (event) => { 22 | if (event.target instanceof HTMLInputElement) { 23 | email = event.target.value; 24 | } 25 | }); 26 | passwordInput.addEventListener('change', (event) => { 27 | if (event.target instanceof HTMLInputElement) { 28 | password = event.target.value; 29 | } 30 | }); 31 | loginForm.addEventListener('submit', async (event) => { 32 | const user = await login(email, password); 33 | 34 | if (user.role === 'admin') { 35 | console.log(`Logged in as ${getUserName(user)} [admin].`); 36 | } else { 37 | console.log(`Logged in as ${getUserName(user)}`); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-basic/src/model/Role.ts: -------------------------------------------------------------------------------- 1 | type Role = 'admin' | 'client' | 'provider'; 2 | 3 | export { Role }; 4 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-basic/src/model/User.ts: -------------------------------------------------------------------------------- 1 | import { Role } from './Role'; 2 | 3 | type User = { 4 | id: string; 5 | email: string; 6 | role: Role; 7 | firstName?: string; 8 | lastName?: string; 9 | }; 10 | 11 | function getUserName(user: User): string { 12 | return [user.firstName, user.lastName].filter((name) => name !== undefined).join(' '); 13 | } 14 | 15 | export { User, getUserName }; 16 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["ES6", "DOM"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "skipDefaultLibCheck": true, 10 | "strict": true, 11 | "baseUrl": "./src", 12 | "outDir": "./dist" 13 | }, 14 | "include": ["./src"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/index.ts', 6 | output: { 7 | filename: 'index.js', 8 | path: path.resolve(__dirname, 'dist'), 9 | hashFunction: 'xxhash64', // @see https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.tsx?$/, 15 | loader: 'ts-loader', 16 | exclude: /node_modules/, 17 | options: { 18 | transpileOnly: true, 19 | }, 20 | }, 21 | ], 22 | }, 23 | resolve: { 24 | extensions: ['.tsx', '.ts', '.js'], 25 | }, 26 | plugins: [ 27 | new ForkTsCheckerWebpackPlugin({ 28 | async: false, 29 | }), 30 | ], 31 | infrastructureLogging: { 32 | level: 'log', 33 | debug: /ForkTsCheckerWebpackPlugin/, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-monorepo", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "dependencies": { 10 | "@babel/core": "^7.10.0", 11 | "@babel/preset-env": "^7.10.0", 12 | "@babel/preset-typescript": "^7.9.0", 13 | "babel-loader": "^8.1.0", 14 | "typescript": "^3.8.0", 15 | "webpack": "^5.54.0", 16 | "webpack-cli": "^4.0.0", 17 | "webpack-dev-server": "^3.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-references-fixture/client", 3 | "license": "MIT", 4 | "version": "1.0.0", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "dependencies": { 11 | "@project-references-fixture/shared": "1.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { intersect, subtract } from '@project-references-fixture/shared'; 2 | 3 | function compute(arrayA: T[], arrayB: T[]) { 4 | const intersection = intersect(arrayA, arrayB); 5 | const difference = subtract(arrayA, arrayB); 6 | 7 | return { 8 | intersection, 9 | difference, 10 | }; 11 | } 12 | 13 | const { intersection, difference } = compute(['a', 'b', 'c'], ['a', 'd', 'a']); 14 | 15 | console.log(`Intersection: ${JSON.stringify(intersection)}`); 16 | console.log(`Difference: ${JSON.stringify(difference)}`); 17 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib", 6 | "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", 7 | "sourceRoot": "./src", 8 | "baseUrl": "./src" 9 | }, 10 | "references": [{ "path": "../shared" }], 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-references-fixture/shared", 3 | "license": "MIT", 4 | "version": "1.0.0", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | import intersect from './intersect'; 2 | import subtract from './subtract'; 3 | 4 | export { intersect, subtract }; 5 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/packages/shared/src/intersect.ts: -------------------------------------------------------------------------------- 1 | function intersect(arrayA: T[] = [], arrayB: T[] = []): T[] { 2 | return arrayA.filter((item) => arrayB.includes(item)); 3 | } 4 | 5 | export default intersect; 6 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/packages/shared/src/subtract.ts: -------------------------------------------------------------------------------- 1 | function subtract(arrayA: T[] = [], arrayB: T[] = []): T[] { 2 | return arrayA.filter((item) => !arrayB.includes(item)); 3 | } 4 | 5 | export default subtract; 6 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib", 6 | "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", 7 | "sourceRoot": "./src", 8 | "baseUrl": "./src" 9 | }, 10 | "include": ["src"] 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es5", "scripthost"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "importHelpers": false, 9 | "skipLibCheck": true, 10 | "skipDefaultLibCheck": true, 11 | "strict": true, 12 | "baseUrl": ".", 13 | "composite": true, 14 | "incremental": true, 15 | "declaration": true, 16 | "declarationMap": true, 17 | "sourceMap": true, 18 | "rootDir": "./packages" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "files": [], 4 | "references": [ 5 | { "path": "./packages/shared" }, 6 | { "path": "./packages/client" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-monorepo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './packages/client/src/index.ts', 6 | output: { 7 | filename: 'index.js', 8 | path: path.resolve(__dirname, 'dist'), 9 | hashFunction: 'xxhash64', // @see https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.ts$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 19 | }, 20 | }, 21 | ], 22 | }, 23 | resolve: { 24 | extensions: ['.ts', '.js'], 25 | // as babel-loader doesn't support project references we put an alias to resolve package 26 | // and to not pollute output with babel-loader errors 27 | alias: { 28 | '@project-references-fixture/shared': path.resolve(__dirname, 'packages/shared/src'), 29 | }, 30 | }, 31 | plugins: [ 32 | new ForkTsCheckerWebpackPlugin({ 33 | async: false, 34 | typescript: { 35 | build: true, 36 | mode: 'readonly', 37 | }, 38 | }), 39 | ], 40 | infrastructureLogging: { 41 | level: 'log', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-package/package/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare function sayHello(who: string): string; 2 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-package/package/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.sayHello = void 0; 4 | function sayHello(who) { 5 | return 'Hello ' + who + '!'; 6 | } 7 | exports.sayHello = sayHello; 8 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-package/package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-nested-project", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "main": "./index.js", 6 | "types": "./index.d.ts", 7 | "files": [ 8 | "./index.js" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-pnp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-pnp", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "pnp-webpack-plugin": "^1.6.4", 8 | "ts-loader": "^5.0.0", 9 | "typescript": "^3.8.0", 10 | "webpack": "^5.54.0", 11 | "webpack-cli": "^4.0.0", 12 | "webpack-dev-server": "^3.0.0" 13 | }, 14 | "installConfig": { 15 | "pnp": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-pnp/src/authenticate.ts: -------------------------------------------------------------------------------- 1 | import { User } from './model/User'; 2 | 3 | async function login(email: string, password: string): Promise { 4 | const response = await fetch('/login', { 5 | method: 'POST', 6 | body: JSON.stringify({ email, password }), 7 | }); 8 | return response.json(); 9 | } 10 | 11 | async function logout(): Promise { 12 | const response = await fetch('/logout', { 13 | method: 'POST', 14 | }); 15 | return response.json(); 16 | } 17 | 18 | export { login, logout }; 19 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-pnp/src/index.ts: -------------------------------------------------------------------------------- 1 | import { login } from './authenticate'; 2 | import { getUserName } from './model/User'; 3 | 4 | const emailInput = document.getElementById('email'); 5 | const passwordInput = document.getElementById('password'); 6 | const loginForm = document.getElementById('login'); 7 | 8 | if (!emailInput) { 9 | throw new Error('Cannot find #email input.'); 10 | } 11 | if (!passwordInput) { 12 | throw new Error('Cannot find #password input.'); 13 | } 14 | if (!loginForm) { 15 | throw new Error('Cannot find #login form.'); 16 | } 17 | 18 | let email = ''; 19 | let password = ''; 20 | 21 | emailInput.addEventListener('change', (event) => { 22 | if (event.target instanceof HTMLInputElement) { 23 | email = event.target.value; 24 | } 25 | }); 26 | passwordInput.addEventListener('change', (event) => { 27 | if (event.target instanceof HTMLInputElement) { 28 | password = event.target.value; 29 | } 30 | }); 31 | loginForm.addEventListener('submit', async (event) => { 32 | const user = await login(email, password); 33 | 34 | if (user.role === 'admin') { 35 | console.log(`Logged in as ${getUserName(user)} [admin].`); 36 | } else { 37 | console.log(`Logged in as ${getUserName(user)}`); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-pnp/src/model/Role.ts: -------------------------------------------------------------------------------- 1 | type Role = 'admin' | 'client' | 'provider'; 2 | 3 | export { Role }; 4 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-pnp/src/model/User.ts: -------------------------------------------------------------------------------- 1 | import { Role } from './Role'; 2 | 3 | type User = { 4 | id: string; 5 | email: string; 6 | role: Role; 7 | firstName?: string; 8 | lastName?: string; 9 | }; 10 | 11 | function getUserName(user: User): string { 12 | return [user.firstName, user.lastName].filter((name) => name !== undefined).join(' '); 13 | } 14 | 15 | export { User, getUserName }; 16 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-pnp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["ES6", "DOM"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "skipDefaultLibCheck": true, 10 | "strict": true, 11 | "baseUrl": "./src", 12 | "outDir": "./dist" 13 | }, 14 | "include": ["./src"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /test/e2e/fixtures/typescript-pnp/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const PnpWebpackPlugin = require('pnp-webpack-plugin'); 3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './src/index.ts', 7 | output: { 8 | filename: 'index.js', 9 | path: path.resolve(__dirname, 'dist'), 10 | hashFunction: 'xxhash64', // @see https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.tsx?$/, 16 | loader: 'ts-loader', 17 | exclude: /node_modules/, 18 | options: PnpWebpackPlugin.tsLoaderOptions({ 19 | transpileOnly: true, 20 | }), 21 | }, 22 | ], 23 | }, 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | plugins: [PnpWebpackPlugin], 27 | }, 28 | resolveLoader: { 29 | plugins: [PnpWebpackPlugin.moduleLoader(module)], 30 | }, 31 | plugins: [ 32 | new ForkTsCheckerWebpackPlugin({ 33 | async: false, 34 | }), 35 | ], 36 | infrastructureLogging: { 37 | level: 'log', 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /test/e2e/fixtures/webpack-node-api/webpack-node-api.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const configuration = require('./webpack.config.js'); 3 | 4 | const builder = webpack({ ...configuration, mode: 'development' }); 5 | 6 | function run() { 7 | return new Promise((resolve, reject) => { 8 | builder.run((error, stats) => { 9 | if (error) { 10 | reject(error); 11 | } else { 12 | resolve(stats); 13 | } 14 | }); 15 | }); 16 | } 17 | 18 | function runAndPrint() { 19 | return run() 20 | .then((stats) => { 21 | const warnings = stats.compilation.warnings; 22 | const errors = stats.compilation.errors; 23 | 24 | if (warnings.length === 0 && errors.length === 0) { 25 | console.log('Compiled successfully.'); 26 | } else { 27 | console.log('Compiled with warnings or errors.'); 28 | } 29 | }) 30 | .catch((error) => console.error(error)); 31 | } 32 | 33 | // run build twice in sequence 34 | runAndPrint() 35 | .then(() => runAndPrint()) 36 | .then(() => console.log('Compiled successfully twice.')); 37 | -------------------------------------------------------------------------------- /test/e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: './jest.environment.js', 4 | testRunner: 'jest-circus/runner', 5 | testTimeout: 300000, 6 | verbose: true, 7 | setupFilesAfterEnv: ['./jest.setup.ts'], 8 | rootDir: '.', 9 | moduleNameMapper: { 10 | '^lib/(.*)$': '/../../lib/$1', 11 | }, 12 | globals: { 13 | 'ts-jest': { 14 | tsconfig: '/tsconfig.json', 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /test/e2e/jest.environment.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const NodeEnvironment = require('jest-environment-node'); 3 | 4 | function printBlock(message) { 5 | process.stdout.write('┏━' + '━'.repeat(message.length) + '━┓\n'); 6 | process.stdout.write('┃ ' + message + ' ┃\n'); 7 | process.stdout.write('┗━' + '━'.repeat(message.length) + '━┛\n'); 8 | } 9 | 10 | class E2EEnvironment extends NodeEnvironment { 11 | constructor(config) { 12 | super(config); 13 | } 14 | 15 | async handleTestEvent(event) { 16 | switch (event.name) { 17 | case 'test_start': 18 | printBlock(`Test Start: ${event.test.name}`); 19 | break; 20 | 21 | case 'test_retry': 22 | printBlock(`Test Retry: ${event.test.name}`); 23 | break; 24 | 25 | case 'test_done': 26 | printBlock(`Test Done: ${event.test.name} (${Math.round(event.test.duration / 1000)}s)`); 27 | break; 28 | } 29 | } 30 | } 31 | 32 | module.exports = E2EEnvironment; 33 | -------------------------------------------------------------------------------- /test/e2e/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { createSandbox, packLocalPackage } from 'karton'; 4 | import type { Sandbox } from 'karton'; 5 | 6 | declare global { 7 | let sandbox: Sandbox; 8 | // eslint-disable-next-line @typescript-eslint/no-namespace 9 | namespace NodeJS { 10 | interface Global { 11 | sandbox: Sandbox; 12 | } 13 | } 14 | } 15 | 16 | beforeAll(async () => { 17 | const forkTsCheckerWebpackPluginTar = await packLocalPackage(path.resolve(__dirname, '../../')); 18 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 19 | // @ts-ignore 20 | global.sandbox = await createSandbox({ 21 | lockDirectory: path.resolve(__dirname, '__locks__'), 22 | fixedDependencies: { 23 | 'fork-ts-checker-webpack-plugin': `file:${forkTsCheckerWebpackPluginTar}`, 24 | }, 25 | }); 26 | }); 27 | 28 | beforeEach(async () => { 29 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 30 | // @ts-ignore 31 | await global.sandbox.reset(); 32 | }); 33 | 34 | afterAll(async () => { 35 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 36 | // @ts-ignore 37 | await global.sandbox.cleanup(); 38 | }); 39 | 40 | jest.retryTimes(3); 41 | -------------------------------------------------------------------------------- /test/e2e/out-of-memory-and-cosmiconfig.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { createProcessDriver } from 'karton'; 4 | 5 | describe('ForkTsCheckerWebpackPlugin Out Of Memory and Cosmiconfig', () => { 6 | it.each([{ async: false }, { async: true }])( 7 | 'handles out of memory for %p', 8 | async ({ async }) => { 9 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 10 | await sandbox.install('yarn', {}); 11 | await sandbox.patch('webpack.config.js', 'async: false,', `async: ${JSON.stringify(async)},`); 12 | 13 | await sandbox.write( 14 | 'fork-ts-checker.config.js', 15 | `module.exports = { typescript: { memoryLimit: 10 } };` 16 | ); 17 | 18 | const driver = createProcessDriver(sandbox.spawn('yarn webpack serve --mode=development')); 19 | 20 | // we should see an error message about out of memory 21 | await driver.waitForStderrIncludes( 22 | 'Issues checking service aborted - probably out of memory. Check the `memoryLimit` option in the ForkTsCheckerWebpackPlugin configuration.\n' + 23 | "If increasing the memory doesn't solve the issue, it's most probably a bug in the TypeScript." 24 | ); 25 | 26 | // let's modify one file to check if plugin will try to restart the service 27 | await sandbox.patch( 28 | 'src/index.ts', 29 | "import { getUserName } from './model/User';", 30 | "import { getUserName } from './model/User';\n" 31 | ); 32 | 33 | // we should see an error message about out of memory again 34 | await driver.waitForStderrIncludes( 35 | 'Issues checking service aborted - probably out of memory. Check the `memoryLimit` option in the ForkTsCheckerWebpackPlugin configuration.\n' + 36 | "If increasing the memory doesn't solve the issue, it's most probably a bug in the TypeScript." 37 | ); 38 | } 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /test/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["../../lib", "./*.spec.ts", "./jest.setup.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/type-definitions.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | describe('Type Definitions', () => { 4 | it('provides valid type definitions', async () => { 5 | await sandbox.load(path.join(__dirname, 'fixtures/type-definitions')); 6 | await sandbox.install('yarn', {}); 7 | 8 | expect(await sandbox.exec('yarn tsc', { fail: true })).toContain( 9 | "webpack.config.ts(10,7): error TS2322: Type 'string' is not assignable to type 'boolean | undefined'." 10 | ); 11 | 12 | await sandbox.patch('webpack.config.ts', "async: 'invalid_value'", 'async: true'); 13 | 14 | expect(await sandbox.exec('yarn tsc')).not.toContain('error TS'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/e2e/type-script-config.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import type { WebpackDevServerDriver } from './driver/webpack-dev-server-driver'; 4 | import { createWebpackDevServerDriver } from './driver/webpack-dev-server-driver'; 5 | 6 | describe('TypeScript Config', () => { 7 | it.each([ 8 | { async: true, typescript: '~3.6.0', 'ts-loader': '^7.0.0' }, 9 | { async: false, typescript: '~3.8.0', 'ts-loader': '^8.0.0' }, 10 | { async: true, typescript: '~4.0.0', 'ts-loader': '^8.0.0' }, 11 | { async: false, typescript: '~4.3.0', 'ts-loader': '^8.0.0' }, 12 | ])( 13 | 'change in the tsconfig.json affects compilation for %p', 14 | async ({ async, ...dependencies }) => { 15 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 16 | await sandbox.install('yarn', { ...dependencies }); 17 | await sandbox.patch('webpack.config.js', 'async: false,', `async: ${JSON.stringify(async)},`); 18 | 19 | const driver = createWebpackDevServerDriver( 20 | sandbox.spawn('yarn webpack serve --mode=development'), 21 | async 22 | ); 23 | 24 | // first compilation is successful 25 | await driver.waitForNoErrors(); 26 | 27 | // change available libraries 28 | await sandbox.patch('tsconfig.json', '"lib": ["ES6", "DOM"]', '"lib": ["ES6"],'); 29 | 30 | const errors = await driver.waitForErrors(); 31 | expect(errors.length).toBeGreaterThan(0); 32 | 33 | // revert the change 34 | await sandbox.patch('tsconfig.json', '"lib": ["ES6"],', '"lib": ["DOM", "ES6"]'); 35 | 36 | await driver.waitForNoErrors(); 37 | } 38 | ); 39 | 40 | it.each([ 41 | { typescript: '~3.6.0' }, 42 | { typescript: '^3.8.0' }, 43 | { typescript: '^4.0.0' }, 44 | { typescript: '^4.3.0' }, 45 | ])('reports errors because of the misconfiguration', async (dependencies) => { 46 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 47 | await sandbox.install('yarn', { ...dependencies }); 48 | 49 | let driver: WebpackDevServerDriver; 50 | let errors: string[]; 51 | 52 | await sandbox.write( 53 | 'fork-ts-checker.config.js', 54 | 'module.exports = { typescript: { configOverwrite: { compilerOptions: { target: "ES3", lib: ["ES3"] } } } };' 55 | ); 56 | 57 | driver = createWebpackDevServerDriver( 58 | sandbox.spawn('yarn webpack serve --mode=development'), 59 | false 60 | ); 61 | errors = await driver.waitForErrors(); 62 | expect(errors.length).toBeGreaterThan(0); 63 | await sandbox.kill(driver.process); 64 | 65 | await sandbox.write( 66 | 'fork-ts-checker.config.js', 67 | 'module.exports = { typescript: { configOverwrite: { include: [] } } };' 68 | ); 69 | 70 | driver = createWebpackDevServerDriver( 71 | sandbox.spawn('yarn webpack serve --mode=development'), 72 | false 73 | ); 74 | errors = await driver.waitForErrors(); 75 | expect(errors.length).toBeGreaterThan(0); 76 | await sandbox.kill(driver.process); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/e2e/type-script-context-option.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { createWebpackDevServerDriver } from './driver/webpack-dev-server-driver'; 4 | 5 | describe('TypeScript Context Option', () => { 6 | it.each([ 7 | { async: true, typescript: '~3.6.0' }, 8 | { async: false, typescript: '~3.8.0' }, 9 | { async: true, typescript: '~4.0.0' }, 10 | { async: false, typescript: '~4.3.0' }, 11 | ])('uses context and cwd to resolve project files for %p', async ({ async, typescript }) => { 12 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 13 | await sandbox.install('yarn', { typescript }); 14 | await sandbox.patch( 15 | 'webpack.config.js', 16 | ' async: false,', 17 | [ 18 | ` async: ${JSON.stringify(async)},`, 19 | ' typescript: {', 20 | ' enabled: true,', 21 | ' configFile: path.resolve(__dirname, "build/tsconfig.json"),', 22 | ' context: __dirname,', 23 | ' },', 24 | ].join('\n') 25 | ); 26 | 27 | // update sandbox to use context option 28 | await sandbox.remove('tsconfig.json'); 29 | await sandbox.write( 30 | 'build/tsconfig.json', 31 | JSON.stringify({ 32 | compilerOptions: { 33 | target: 'es5', 34 | module: 'commonjs', 35 | lib: ['ES6', 'DOM'], 36 | moduleResolution: 'node', 37 | esModuleInterop: true, 38 | skipLibCheck: true, 39 | skipDefaultLibCheck: true, 40 | strict: true, 41 | baseUrl: './src', 42 | outDir: './dist', 43 | }, 44 | include: ['./src'], 45 | exclude: ['node_modules'], 46 | }) 47 | ); 48 | await sandbox.patch( 49 | 'webpack.config.js', 50 | "entry: './src/index.ts',", 51 | ["entry: './src/index.ts',", 'context: path.resolve(__dirname),'].join('\n') 52 | ); 53 | await sandbox.patch( 54 | 'webpack.config.js', 55 | ' transpileOnly: true,', 56 | [ 57 | ' transpileOnly: true,', 58 | ' configFile: path.resolve(__dirname, "build/tsconfig.json"),', 59 | ' context: __dirname,', 60 | ].join('\n') 61 | ); 62 | // create additional directory for cwd test 63 | await sandbox.write('foo/.gitignore', ''); 64 | 65 | const driver = createWebpackDevServerDriver( 66 | sandbox.spawn(`yarn webpack serve --mode=development --config=../webpack.config.js`, { 67 | cwd: path.join(sandbox.context, 'foo'), 68 | }), 69 | async 70 | ); 71 | 72 | // first compilation is successful 73 | await driver.waitForNoErrors(); 74 | 75 | // then we introduce semantic error by removing "firstName" and "lastName" from the User model 76 | await sandbox.patch( 77 | 'src/model/User.ts', 78 | [' firstName?: string;', ' lastName?: string;'].join('\n'), 79 | '' 80 | ); 81 | 82 | // we should receive 2 semantic errors 83 | const errors = await driver.waitForErrors(); 84 | expect(errors).toEqual([ 85 | [ 86 | 'ERROR in ../src/model/User.ts:11:16', 87 | "TS2339: Property 'firstName' does not exist on type 'User'.", 88 | ' 9 |', 89 | ' 10 | function getUserName(user: User): string {', 90 | " > 11 | return [user.firstName, user.lastName].filter((name) => name !== undefined).join(' ');", 91 | ' | ^^^^^^^^^', 92 | ' 12 | }', 93 | ' 13 |', 94 | ' 14 | export { User, getUserName };', 95 | ].join('\n'), 96 | [ 97 | 'ERROR in ../src/model/User.ts:11:32', 98 | "TS2339: Property 'lastName' does not exist on type 'User'.", 99 | ' 9 |', 100 | ' 10 | function getUserName(user: User): string {', 101 | " > 11 | return [user.firstName, user.lastName].filter((name) => name !== undefined).join(' ');", 102 | ' | ^^^^^^^^', 103 | ' 12 | }', 104 | ' 13 |', 105 | ' 14 | export { User, getUserName };', 106 | ].join('\n'), 107 | ]); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/e2e/type-script-formatter-option.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { createWebpackDevServerDriver } from './driver/webpack-dev-server-driver'; 4 | 5 | describe('TypeScript Formatter Option', () => { 6 | it.each([ 7 | { async: true, typescript: '~3.6.0' }, 8 | { async: false, typescript: '~4.3.0' }, 9 | ])( 10 | 'uses the custom formatter to format the error message for %p', 11 | async ({ async, typescript }) => { 12 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 13 | await sandbox.install('yarn', { typescript }); 14 | await sandbox.patch( 15 | 'webpack.config.js', 16 | ' async: false,', 17 | [ 18 | ` async: ${JSON.stringify(async)},`, 19 | ' formatter: (issue) => {', 20 | ' return `It is the custom issue statement - ${issue.code}: ${issue.message}`', 21 | ' },', 22 | ].join('\n') 23 | ); 24 | 25 | const driver = createWebpackDevServerDriver( 26 | sandbox.spawn('yarn webpack serve --mode=development'), 27 | async 28 | ); 29 | 30 | // first compilation is successful 31 | await driver.waitForNoErrors(); 32 | 33 | // then we introduce semantic error by removing "firstName" and "lastName" from the User model 34 | await sandbox.patch( 35 | 'src/model/User.ts', 36 | [' firstName?: string;', ' lastName?: string;'].join('\n'), 37 | '' 38 | ); 39 | 40 | // we should receive 2 semantic errors 41 | const errors = await driver.waitForErrors(); 42 | expect(errors).toEqual([ 43 | [ 44 | 'ERROR in ./src/model/User.ts:11:16', 45 | "It is the custom issue statement - TS2339: Property 'firstName' does not exist on type 'User'.", 46 | ].join('\n'), 47 | [ 48 | 'ERROR in ./src/model/User.ts:11:32', 49 | "It is the custom issue statement - TS2339: Property 'lastName' does not exist on type 'User'.", 50 | ].join('\n'), 51 | ]); 52 | } 53 | ); 54 | }); 55 | -------------------------------------------------------------------------------- /test/e2e/type-script-tracing.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { extractWebpackErrors } from './driver/webpack-errors-extractor'; 4 | 5 | describe('TypeScript Tracing', () => { 6 | it.each([ 7 | { build: false, typescript: '~4.3.0' }, 8 | { build: true, typescript: '~4.3.0' }, 9 | { build: false, typescript: '~4.6.0' }, 10 | { build: true, typescript: '~4.6.0' }, 11 | ])('can generate trace files for %p', async ({ build, ...dependencies }) => { 12 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 13 | await sandbox.install('yarn', { ...dependencies }); 14 | 15 | // enable tracing 16 | await sandbox.patch( 17 | 'tsconfig.json', 18 | '"outDir": "./dist"', 19 | '"outDir": "./dist",\n"generateTrace": "./traces"' 20 | ); 21 | 22 | await sandbox.write( 23 | 'fork-ts-checker.config.js', 24 | `module.exports = ${JSON.stringify({ typescript: { build } })};` 25 | ); 26 | 27 | const webpackResult = await sandbox.exec('yarn webpack --mode=development'); 28 | const errors = extractWebpackErrors(webpackResult); 29 | expect(errors).toEqual([]); 30 | 31 | expect(await sandbox.exists('dist')).toEqual(true); 32 | 33 | expect(await sandbox.list('./traces')).toEqual( 34 | expect.arrayContaining([ 35 | expect.objectContaining({ name: expect.stringMatching(/types.*\.json/) }), 36 | expect.objectContaining({ name: expect.stringMatching(/trace.*\.json/) }), 37 | ]) 38 | ); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/e2e/webpack-inclusive-watcher.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { createProcessDriver } from 'karton'; 4 | 5 | import { createWebpackDevServerDriver } from './driver/webpack-dev-server-driver'; 6 | 7 | describe('Webpack Inclusive Watcher', () => { 8 | it.each([{ async: false }, { async: true }])( 9 | 'ignores package.json change for %p', 10 | async ({ async }) => { 11 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 12 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-package')); 13 | await sandbox.install('yarn', { typescript: '4.6.3' }); 14 | await sandbox.patch('webpack.config.js', 'async: false,', `async: ${JSON.stringify(async)},`); 15 | 16 | // add import to typescript-nested-project project 17 | await sandbox.patch( 18 | 'src/index.ts', 19 | "import { getUserName } from './model/User';", 20 | [ 21 | "import { getUserName } from './model/User';", 22 | 'import { sayHello } from "../package";', 23 | '', 24 | "sayHello('World');", 25 | ].join('\n') 26 | ); 27 | 28 | // start webpack dev server 29 | const process = sandbox.spawn('yarn webpack serve --mode=development'); 30 | const baseDriver = createProcessDriver(process); 31 | const webpackDriver = createWebpackDevServerDriver(process, async); 32 | 33 | await webpackDriver.waitForNoErrors(); 34 | 35 | // update nested package.json file 36 | await sandbox.patch('package/package.json', '"1.0.0"', '"1.0.1"'); 37 | 38 | // wait for 5 seconds and fail if there is Debug Failure. in the console output 39 | await expect(() => 40 | baseDriver.waitForStderrIncludes('Error: Debug Failure.', 5000) 41 | ).rejects.toEqual( 42 | new Error('Exceeded time on waiting for "Error: Debug Failure." to appear in the stderr.') 43 | ); 44 | 45 | await webpackDriver.waitForNoErrors(); 46 | } 47 | ); 48 | }); 49 | -------------------------------------------------------------------------------- /test/e2e/webpack-node-api.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | describe('Webpack Node Api', () => { 4 | it.each([{ webpack: '5.54.0' }, { webpack: '^5.54.0' }])( 5 | 'compiles the project successfully with %p', 6 | async (dependencies) => { 7 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 8 | await sandbox.load(path.join(__dirname, 'fixtures/webpack-node-api')); 9 | await sandbox.install('yarn', { ...dependencies }); 10 | 11 | const result = await sandbox.exec('yarn node ./webpack-node-api.js'); 12 | expect(result).toContain('Compiled successfully twice.'); 13 | } 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /test/e2e/webpack-production-build.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import stripAnsi from 'strip-ansi'; 4 | 5 | import { extractWebpackErrors } from './driver/webpack-errors-extractor'; 6 | 7 | describe('Webpack Production Build', () => { 8 | it.each([{ webpack: '5.54.0' }, { webpack: '^5.54.0' }])( 9 | 'compiles the project successfully with %p', 10 | async (dependencies) => { 11 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 12 | await sandbox.install('yarn', { ...dependencies }); 13 | 14 | // lets remove the async option at all as the plugin should now how to set it by default 15 | await sandbox.patch( 16 | 'webpack.config.js', 17 | [' new ForkTsCheckerWebpackPlugin({', ' async: false,', ' }),'].join('\n'), 18 | [' new ForkTsCheckerWebpackPlugin(),'].join('\n') 19 | ); 20 | 21 | const result = await sandbox.exec('yarn webpack --mode=production'); 22 | const errors = extractWebpackErrors(result); 23 | 24 | expect(errors).toEqual([]); 25 | 26 | // check if files has been created 27 | expect(await sandbox.exists('dist')).toEqual(true); 28 | expect(await sandbox.exists('dist/index.d.ts')).toEqual(false); 29 | expect(await sandbox.exists('dist/index.js')).toEqual(true); 30 | expect(await sandbox.exists('dist/authenticate.d.ts')).toEqual(false); 31 | expect(await sandbox.exists('dist/model/User.d.ts')).toEqual(false); 32 | expect(await sandbox.exists('dist/model/Role.d.ts')).toEqual(false); 33 | 34 | await sandbox.remove('dist'); 35 | } 36 | ); 37 | 38 | it('generates .d.ts files in write-dts mode', async () => { 39 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 40 | await sandbox.install('yarn', { webpack: '^5.54.0' }); 41 | 42 | await sandbox.patch( 43 | 'webpack.config.js', 44 | 'async: false,', 45 | 'async: false, typescript: { mode: "write-dts" },' 46 | ); 47 | 48 | const result = await sandbox.exec('yarn webpack --mode=production'); 49 | const errors = extractWebpackErrors(result); 50 | 51 | expect(errors).toEqual([]); 52 | 53 | // check if files has been created 54 | expect(await sandbox.exists('dist')).toEqual(true); 55 | expect(await sandbox.exists('dist/index.d.ts')).toEqual(true); 56 | expect(await sandbox.exists('dist/index.js')).toEqual(true); 57 | expect(await sandbox.exists('dist/authenticate.d.ts')).toEqual(true); 58 | expect(await sandbox.exists('dist/model/User.d.ts')).toEqual(true); 59 | expect(await sandbox.exists('dist/model/Role.d.ts')).toEqual(true); 60 | 61 | await sandbox.remove('dist'); 62 | }); 63 | 64 | it.each([{ webpack: '5.54.0' }, { webpack: '^5.54.0' }])( 65 | 'exits with error on the project error with %p', 66 | async (dependencies) => { 67 | await sandbox.load(path.join(__dirname, 'fixtures/typescript-basic')); 68 | await sandbox.install('yarn', { ...dependencies }); 69 | 70 | // remove the async option at all as the plugin should now how to set it by default 71 | await sandbox.patch( 72 | 'webpack.config.js', 73 | [' new ForkTsCheckerWebpackPlugin({', ' async: false,', ' }),'].join('\n'), 74 | [' new ForkTsCheckerWebpackPlugin(),'].join('\n') 75 | ); 76 | 77 | // introduce an error in the project 78 | await sandbox.remove('src/model/User.ts'); 79 | const result = await sandbox.exec('yarn webpack --mode=production', { fail: true }); 80 | 81 | // remove npm related output 82 | const output = stripAnsi(String(result)).replace(/npm (ERR!|WARN).*/g, ''); 83 | // extract errors 84 | const errors = extractWebpackErrors(output); 85 | 86 | expect(errors).toEqual([ 87 | // first error is from the webpack module resolution 88 | expect.anything(), 89 | [ 90 | 'ERROR in ./src/authenticate.ts:1:22', 91 | "TS2307: Cannot find module './model/User' or its corresponding type declarations.", 92 | " > 1 | import { User } from './model/User';", 93 | ' | ^^^^^^^^^^^^^^', 94 | ' 2 |', 95 | ' 3 | async function login(email: string, password: string): Promise {', 96 | " 4 | const response = await fetch('/login', {", 97 | ].join('\n'), 98 | [ 99 | 'ERROR in ./src/index.ts:2:29', 100 | "TS2307: Cannot find module './model/User' or its corresponding type declarations.", 101 | " 1 | import { login } from './authenticate';", 102 | " > 2 | import { getUserName } from './model/User';", 103 | ' | ^^^^^^^^^^^^^^', 104 | ' 3 |', 105 | " 4 | const emailInput = document.getElementById('email');", 106 | " 5 | const passwordInput = document.getElementById('password');", 107 | ].join('\n'), 108 | ]); 109 | } 110 | ); 111 | }); 112 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["ES6"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "skipDefaultLibCheck": true, 10 | "resolveJsonModule": true, 11 | "baseUrl": "../" 12 | }, 13 | "include": ["../src", "**/*.spec.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /test/unit/files-change.spec.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | import { 4 | clearFilesChange, 5 | consumeFilesChange, 6 | getFilesChange, 7 | updateFilesChange, 8 | } from '../../src/files-change'; 9 | 10 | describe('files-change', () => { 11 | let compiler: webpack.Compiler; 12 | let otherCompiler: webpack.Compiler; 13 | 14 | beforeEach(() => { 15 | // compiler is used only as a key 16 | compiler = {} as unknown as webpack.Compiler; 17 | otherCompiler = {} as unknown as webpack.Compiler; 18 | }); 19 | 20 | it('gets files change without modifying them', () => { 21 | updateFilesChange(compiler, { changedFiles: ['foo.ts'], deletedFiles: ['bar.ts'] }); 22 | 23 | expect(getFilesChange(compiler)).toEqual({ 24 | changedFiles: ['foo.ts'], 25 | deletedFiles: ['bar.ts'], 26 | }); 27 | expect(getFilesChange(compiler)).toEqual({ 28 | changedFiles: ['foo.ts'], 29 | deletedFiles: ['bar.ts'], 30 | }); 31 | }); 32 | 33 | it('aggregates changes', () => { 34 | updateFilesChange(compiler, { changedFiles: ['foo.ts', 'baz.ts'], deletedFiles: ['bar.ts'] }); 35 | updateFilesChange(compiler, { changedFiles: ['bar.ts'], deletedFiles: [] }); 36 | updateFilesChange(compiler, { changedFiles: [], deletedFiles: ['baz.ts'] }); 37 | 38 | expect(getFilesChange(compiler)).toEqual({ 39 | changedFiles: ['foo.ts', 'bar.ts'], 40 | deletedFiles: ['baz.ts'], 41 | }); 42 | }); 43 | 44 | it('clears changes', () => { 45 | updateFilesChange(compiler, { changedFiles: ['foo.ts', 'baz.ts'], deletedFiles: ['bar.ts'] }); 46 | clearFilesChange(compiler); 47 | 48 | expect(getFilesChange(compiler)).toEqual({ changedFiles: [], deletedFiles: [] }); 49 | }); 50 | 51 | it('consumes changes', () => { 52 | updateFilesChange(compiler, { changedFiles: ['foo.ts', 'baz.ts'], deletedFiles: ['bar.ts'] }); 53 | 54 | expect(consumeFilesChange(compiler)).toEqual({ 55 | changedFiles: ['foo.ts', 'baz.ts'], 56 | deletedFiles: ['bar.ts'], 57 | }); 58 | expect(getFilesChange(compiler)).toEqual({ changedFiles: [], deletedFiles: [] }); 59 | expect(consumeFilesChange(compiler)).toEqual({ changedFiles: [], deletedFiles: [] }); 60 | }); 61 | 62 | it('keeps files changes data per compiler', () => { 63 | updateFilesChange(compiler, { changedFiles: ['foo.ts', 'baz.ts'], deletedFiles: ['bar.ts'] }); 64 | 65 | expect(getFilesChange(otherCompiler)).toEqual({ 66 | changedFiles: [], 67 | deletedFiles: [], 68 | }); 69 | expect(consumeFilesChange(otherCompiler)).toEqual({ 70 | changedFiles: [], 71 | deletedFiles: [], 72 | }); 73 | expect(getFilesChange(compiler)).toEqual({ 74 | changedFiles: ['foo.ts', 'baz.ts'], 75 | deletedFiles: ['bar.ts'], 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/unit/formatter/__mocks__/chalk.js: -------------------------------------------------------------------------------- 1 | function style(...strings) { 2 | // by-pass styling 3 | return strings.join(' '); 4 | } 5 | 6 | const modifiers = [ 7 | 'reset', 8 | 'bold', 9 | 'dim', 10 | 'italic', 11 | 'underline', 12 | 'inverse', 13 | 'hidden', 14 | 'strikethrough', 15 | 'visible', 16 | ]; 17 | const colors = [ 18 | 'black', 19 | 'red', 20 | 'green', 21 | 'yellow', 22 | 'blue', 23 | 'magenta', 24 | 'cyan', 25 | 'white', 26 | 'blackBright', 27 | 'gray', 28 | 'grey', 29 | 'redBright', 30 | 'greenBright', 31 | 'yellowBright', 32 | 'blueBright', 33 | 'magentaBright', 34 | 'cyanBright', 35 | 'whiteBright', 36 | ]; 37 | const bgColors = [ 38 | 'bgBlack', 39 | 'bgRed', 40 | 'bgGreen', 41 | 'bgYellow', 42 | 'bgBlue', 43 | 'bgMagenta', 44 | 'bgCyan', 45 | 'bgWhite', 46 | 'bgBlackBright', 47 | 'bgGray', 48 | 'bgGrey', 49 | 'bgRedBright', 50 | 'bgGreenBright', 51 | 'bgYellowBright', 52 | 'bgBlueBright', 53 | 'bgMagentaBright', 54 | 'bgCyanBright', 55 | 'bgWhiteBright', 56 | ]; 57 | const models = ['rgb', 'hex', 'keyword', 'hsl', 'hsv', 'hwb', 'ansi', 'ansi256']; 58 | 59 | const styleMethods = [...modifiers, ...colors, ...bgColors]; 60 | const modelMethods = models; 61 | 62 | // register all style methods as a chain methods 63 | styleMethods.forEach((method) => { 64 | style[method] = style; 65 | }); 66 | // register all model methods as a chain methods 67 | modelMethods.forEach((method) => { 68 | style[method] = () => style; 69 | }); 70 | 71 | // chalk constructor 72 | function Chalk() { 73 | // chalk API 74 | this.supportsColor = { 75 | level: 0, 76 | hasBasic: false, 77 | has256: false, 78 | has16m: false, 79 | }; 80 | 81 | // register all style methods as a chalk API 82 | styleMethods.forEach((method) => { 83 | this[method] = style; 84 | }); 85 | // register all model methods as a chalk API 86 | modelMethods.forEach((method) => { 87 | this[method] = () => style; 88 | }); 89 | } 90 | 91 | // default chalk instance 92 | const chalk = new Chalk(); 93 | chalk.stderr = new Chalk(); 94 | chalk.Instance = Chalk; 95 | 96 | // mimic chalk export style 97 | chalk.default = chalk; 98 | module.exports = chalk; 99 | -------------------------------------------------------------------------------- /test/unit/formatter/basic-formatter.spec.ts: -------------------------------------------------------------------------------- 1 | import { createBasicFormatter } from 'src/formatter'; 2 | import type { Issue } from 'src/issue'; 3 | 4 | describe('formatter/basic-formatter', () => { 5 | it.each([ 6 | [ 7 | { 8 | severity: 'error', 9 | code: 'TS2322', 10 | message: `Type '"1"' is not assignable to type 'number'.`, 11 | file: 'src/index.ts', 12 | location: { 13 | start: { 14 | line: 10, 15 | column: 4, 16 | }, 17 | end: { 18 | line: 10, 19 | column: 6, 20 | }, 21 | }, 22 | }, 23 | `TS2322: Type '"1"' is not assignable to type 'number'.`, 24 | ], 25 | ])('formats issue message "%p" to "%p"', (...args) => { 26 | const [issue, expectedFormatted] = args as [Issue, string]; 27 | const formatter = createBasicFormatter(); 28 | const formatted = formatter(issue); 29 | 30 | expect(formatted).toEqual(expectedFormatted); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/formatter/code-frame-formatter.spec.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | 3 | import mockFs from 'mock-fs'; 4 | import { createCodeFrameFormatter } from 'src/formatter'; 5 | import type { Issue } from 'src/issue'; 6 | 7 | import { stripAnsi } from './strip-ansi'; 8 | 9 | describe('formatter/code-frame-formatter', () => { 10 | beforeEach(() => { 11 | mockFs({ 12 | src: { 13 | 'index.ts': [ 14 | 'const foo: number = "1";', 15 | 'const bar = 1;', 16 | '', 17 | 'function baz() {', 18 | ' console.log(baz);', 19 | '}', 20 | ].join('\n'), 21 | }, 22 | }); 23 | }); 24 | 25 | afterEach(() => { 26 | mockFs.restore(); 27 | }); 28 | 29 | it.each([ 30 | [ 31 | { 32 | severity: 'error', 33 | code: 'TS2322', 34 | message: `Type '"1"' is not assignable to type 'number'.`, 35 | file: 'src/index.ts', 36 | location: { 37 | start: { 38 | line: 1, 39 | column: 7, 40 | }, 41 | end: { 42 | line: 1, 43 | column: 10, 44 | }, 45 | }, 46 | }, 47 | [ 48 | `TS2322: Type '"1"' is not assignable to type 'number'.`, 49 | ' > 1 | const foo: number = "1";', 50 | ' | ^^^', 51 | ' 2 | const bar = 1;', 52 | ].join(os.EOL), 53 | ], 54 | [ 55 | { 56 | severity: 'error', 57 | code: 'TS2322', 58 | message: `Type '"1"' is not assignable to type 'number'.`, 59 | file: 'src/index.ts', 60 | }, 61 | `TS2322: Type '"1"' is not assignable to type 'number'.`, 62 | ], 63 | [ 64 | { 65 | severity: 'error', 66 | code: 'TS2322', 67 | message: `Type '"1"' is not assignable to type 'number'.`, 68 | file: 'src/index.ts', 69 | location: { 70 | start: { 71 | line: 1, 72 | column: 7, 73 | }, 74 | }, 75 | }, 76 | [ 77 | `TS2322: Type '"1"' is not assignable to type 'number'.`, 78 | ' > 1 | const foo: number = "1";', 79 | ' | ^', 80 | ' 2 | const bar = 1;', 81 | ].join(os.EOL), 82 | ], 83 | [ 84 | { 85 | severity: 'error', 86 | code: 'TS2322', 87 | message: `Type '"1"' is not assignable to type 'number'.`, 88 | file: 'src/not-existing.ts', 89 | location: { 90 | start: { 91 | line: 1, 92 | column: 7, 93 | }, 94 | end: { 95 | line: 1, 96 | column: 10, 97 | }, 98 | }, 99 | }, 100 | `TS2322: Type '"1"' is not assignable to type 'number'.`, 101 | ], 102 | ])('formats issue message "%p" to "%p"', (...args) => { 103 | const [issue, expectedFormatted] = args as [Issue, string]; 104 | const formatter = createCodeFrameFormatter({ 105 | linesAbove: 1, 106 | linesBelow: 1, 107 | }); 108 | const formatted = stripAnsi(formatter(issue)); 109 | 110 | expect(formatted).toEqual(expectedFormatted); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/unit/formatter/formatter-config.spec.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import mockFs from 'mock-fs'; 4 | import type { FormatterOptions } from 'src/formatter'; 5 | import { createFormatterConfig } from 'src/formatter'; 6 | import type { Issue } from 'src/issue'; 7 | 8 | import { stripAnsi } from './strip-ansi'; 9 | 10 | describe('formatter/formatter-config', () => { 11 | beforeEach(() => { 12 | mockFs({ 13 | src: { 14 | 'index.ts': [ 15 | 'const foo: number = "1";', 16 | 'const bar = 1;', 17 | '', 18 | 'function baz() {', 19 | ' console.log(baz);', 20 | '}', 21 | ].join('\n'), 22 | }, 23 | }); 24 | }); 25 | 26 | afterEach(() => { 27 | mockFs.restore(); 28 | }); 29 | 30 | const issue: Issue = { 31 | severity: 'error', 32 | code: 'TS2322', 33 | message: `Type '"1"' is not assignable to type 'number'.`, 34 | file: 'src/index.ts', 35 | location: { 36 | start: { 37 | line: 1, 38 | column: 7, 39 | }, 40 | end: { 41 | line: 1, 42 | column: 10, 43 | }, 44 | }, 45 | }; 46 | 47 | const customFormatter = (issue: Issue) => 48 | `${issue.code}: ${issue.message} at line ${issue.location.start.line}`; 49 | 50 | const BASIC_FORMATTER_OUTPUT = `TS2322: Type '"1"' is not assignable to type 'number'.`; 51 | const CUSTOM_FORMATTER_OUTPUT = `TS2322: Type '"1"' is not assignable to type 'number'. at line 1`; 52 | const CODEFRAME_FORMATTER_OUTPUT = [ 53 | BASIC_FORMATTER_OUTPUT, 54 | ' > 1 | const foo: number = "1";', 55 | ' | ^^^', 56 | ' 2 | const bar = 1;', 57 | ' 3 |', 58 | ' 4 | function baz() {', 59 | ].join(os.EOL); 60 | const CUSTOM_CODEFRAME_FORMATTER_OUTPUT = [ 61 | BASIC_FORMATTER_OUTPUT, 62 | ' > 1 | const foo: number = "1";', 63 | ' | ^^^', 64 | ' 2 | const bar = 1;', 65 | ].join(os.EOL); 66 | 67 | it.each([ 68 | [undefined, CODEFRAME_FORMATTER_OUTPUT, 'relative'], 69 | ['basic', BASIC_FORMATTER_OUTPUT, 'relative'], 70 | [customFormatter, CUSTOM_FORMATTER_OUTPUT, 'relative'], 71 | ['codeframe', CODEFRAME_FORMATTER_OUTPUT, 'relative'], 72 | [{ type: 'basic' }, BASIC_FORMATTER_OUTPUT, 'relative'], 73 | [{ type: 'codeframe' }, CODEFRAME_FORMATTER_OUTPUT, 'relative'], 74 | [ 75 | { type: 'codeframe', options: { linesBelow: 1 } }, 76 | CUSTOM_CODEFRAME_FORMATTER_OUTPUT, 77 | 'relative', 78 | ], 79 | [{ type: 'basic', pathType: 'relative' }, BASIC_FORMATTER_OUTPUT, 'relative'], 80 | [{ type: 'basic', pathType: 'absolute' }, BASIC_FORMATTER_OUTPUT, 'absolute'], 81 | [{ type: 'codeframe', pathType: 'absolute' }, CODEFRAME_FORMATTER_OUTPUT, 'absolute'], 82 | ])('creates configuration from options', (options, expectedFormat, expectedPathType) => { 83 | const formatter = createFormatterConfig(options as FormatterOptions); 84 | const format = stripAnsi(formatter.format(issue)); 85 | 86 | expect(format).toEqual(expectedFormat); 87 | expect(formatter.pathType).toEqual(expectedPathType); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/unit/formatter/strip-ansi.ts: -------------------------------------------------------------------------------- 1 | // Removes ANSI escape codes from a string 2 | // eslint-disable-next-line no-control-regex 3 | export const stripAnsi = (text: string) => text.replace(/\u001b[^m]*?m/g, ''); 4 | -------------------------------------------------------------------------------- /test/unit/formatter/webpack-formatter.spec.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { join } from 'path'; 3 | 4 | import type { Formatter } from 'src/formatter'; 5 | import { createBasicFormatter, createWebpackFormatter } from 'src/formatter'; 6 | import type { Issue } from 'src/issue'; 7 | 8 | import { forwardSlash } from '../../../lib/utils/path/forward-slash'; 9 | 10 | describe('formatter/webpack-formatter', () => { 11 | const issue: Issue = { 12 | severity: 'error', 13 | code: 'TS123', 14 | message: 'Some issue content', 15 | file: join(process.cwd(), 'some/file.ts'), 16 | location: { 17 | start: { 18 | line: 1, 19 | column: 7, 20 | }, 21 | end: { 22 | line: 1, 23 | column: 16, 24 | }, 25 | }, 26 | }; 27 | 28 | let relativeFormatter: Formatter; 29 | let absoluteFormatter: Formatter; 30 | 31 | beforeEach(() => { 32 | relativeFormatter = createWebpackFormatter(createBasicFormatter(), 'relative'); 33 | absoluteFormatter = createWebpackFormatter(createBasicFormatter(), 'absolute'); 34 | }); 35 | 36 | it('decorates existing relativeFormatter', () => { 37 | expect(relativeFormatter(issue)).toContain('TS123: Some issue content'); 38 | }); 39 | 40 | it('formats issue severity', () => { 41 | expect(relativeFormatter({ ...issue, severity: 'error' })).toContain('ERROR'); 42 | expect(relativeFormatter({ ...issue, severity: 'warning' })).toContain('WARNING'); 43 | }); 44 | 45 | it('formats issue file', () => { 46 | expect(relativeFormatter(issue)).toContain(`./some/file.ts`); 47 | expect(absoluteFormatter(issue)).toContain(forwardSlash(`${process.cwd()}/some/file.ts`)); 48 | }); 49 | 50 | it('formats location', () => { 51 | expect(relativeFormatter(issue)).toContain(':1:7'); 52 | expect( 53 | relativeFormatter({ 54 | ...issue, 55 | location: { start: { line: 1, column: 7 }, end: { line: 10, column: 16 } }, 56 | }) 57 | ).toContain(':1:7'); 58 | }); 59 | 60 | it('formats issue header like webpack', () => { 61 | expect(relativeFormatter(issue)).toEqual( 62 | [`ERROR in ./some/file.ts:1:7`, 'TS123: Some issue content', ''].join(os.EOL) 63 | ); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/unit/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testRunner: 'jest-circus/runner', 5 | rootDir: '.', 6 | moduleNameMapper: { 7 | '^src/(.*)$': '/../../src/$1', 8 | }, 9 | globals: { 10 | 'ts-jest': { 11 | tsconfig: '/../tsconfig.json', 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/unit/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { ForkTsCheckerWebpackPlugin } from 'src/plugin'; 2 | 3 | describe('plugin', () => { 4 | it.each([{ invalid: true }, false, null, 'unknown string', { typescript: 'invalid option' }])( 5 | 'throws an error for invalid options %p', 6 | (options) => { 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | expect(() => new ForkTsCheckerWebpackPlugin(options as any)).toThrowError(); 9 | } 10 | ); 11 | 12 | it('exposes current version', () => { 13 | expect(ForkTsCheckerWebpackPlugin.version).toEqual('{{VERSION}}'); // will be replaced by the @semantic-release/exec 14 | }); 15 | 16 | it("doesn't throw an error for empty options", () => { 17 | expect(() => new ForkTsCheckerWebpackPlugin()).not.toThrowError(); 18 | }); 19 | 20 | it('accepts a custom logger', () => { 21 | const logger = { 22 | error: (message) => console.log(message), 23 | log: (message) => console.log(message), 24 | }; 25 | 26 | expect(() => new ForkTsCheckerWebpackPlugin({ logger })).not.toThrowError(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/unit/typescript/type-script-support.spec.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import type { TypeScriptWorkerConfig } from 'src/typescript/type-script-worker-config'; 4 | 5 | describe('typescript/type-script-support', () => { 6 | let configuration: TypeScriptWorkerConfig; 7 | 8 | beforeEach(() => { 9 | jest.resetModules(); 10 | 11 | configuration = { 12 | configFile: './tsconfig.json', 13 | configOverwrite: {}, 14 | context: '.', 15 | build: false, 16 | mode: 'readonly', 17 | diagnosticOptions: { 18 | declaration: false, 19 | global: true, 20 | semantic: true, 21 | syntactic: false, 22 | }, 23 | memoryLimit: 2048, 24 | profile: false, 25 | typescriptPath: require.resolve('typescript'), 26 | }; 27 | }); 28 | 29 | it('throws error if typescript is not installed', async () => { 30 | jest.setMock('typescript', undefined); 31 | 32 | const { assertTypeScriptSupport } = await import('src/typescript/type-script-support'); 33 | 34 | expect(() => assertTypeScriptSupport(configuration)).toThrowError( 35 | 'When you use ForkTsCheckerWebpackPlugin with typescript reporter enabled, you must install `typescript` package.' 36 | ); 37 | }); 38 | 39 | it('throws error if typescript version is lower then 3.6.0', async () => { 40 | jest.setMock('typescript', { version: '3.5.9' }); 41 | 42 | const { assertTypeScriptSupport } = await import('src/typescript/type-script-support'); 43 | 44 | expect(() => assertTypeScriptSupport(configuration)).toThrowError( 45 | [ 46 | `ForkTsCheckerWebpackPlugin cannot use the current typescript version of 3.5.9.`, 47 | 'The minimum required version is 3.6.0.', 48 | ].join(os.EOL) 49 | ); 50 | }); 51 | 52 | it("doesn't throw error if typescript version is greater or equal 3.6.0", async () => { 53 | jest.setMock('typescript', { version: '3.6.0' }); 54 | jest.setMock('fs-extra', { existsSync: () => true }); 55 | 56 | const { assertTypeScriptSupport } = await import('src/typescript/type-script-support'); 57 | 58 | expect(() => assertTypeScriptSupport(configuration)).not.toThrowError(); 59 | }); 60 | 61 | it('throws error if there is no tsconfig.json file', async () => { 62 | jest.setMock('typescript', { version: '3.8.0' }); 63 | jest.setMock('fs-extra', { existsSync: () => false }); 64 | 65 | const { assertTypeScriptSupport } = await import('src/typescript/type-script-support'); 66 | 67 | expect(() => assertTypeScriptSupport(configuration)).toThrowError( 68 | [ 69 | `Cannot find the "./tsconfig.json" file.`, 70 | `Please check webpack and ForkTsCheckerWebpackPlugin configuration.`, 71 | `Possible errors:`, 72 | ' - wrong `context` directory in webpack configuration (if `configFile` is not set or is a relative path in the fork plugin configuration)', 73 | ' - wrong `typescript.configFile` path in the plugin configuration (should be a relative or absolute path)', 74 | ].join(os.EOL) 75 | ); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/unit/typescript/type-script-worker-config.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import type { TypeScriptWorkerConfig } from 'src/typescript/type-script-worker-config'; 4 | import type { TypeScriptWorkerOptions } from 'src/typescript/type-script-worker-options'; 5 | import type * as webpack from 'webpack'; 6 | 7 | describe('typescript/type-scripts-worker-config', () => { 8 | let compiler: webpack.Compiler; 9 | const context = '/webpack/context'; 10 | 11 | const configuration: TypeScriptWorkerConfig = { 12 | memoryLimit: 2048, 13 | configFile: path.normalize(path.resolve(context, 'tsconfig.json')), 14 | configOverwrite: {}, 15 | context: path.normalize(path.dirname(path.resolve(context, 'tsconfig.json'))), 16 | build: false, 17 | mode: 'readonly', 18 | diagnosticOptions: { 19 | semantic: true, 20 | syntactic: false, 21 | declaration: false, 22 | global: false, 23 | }, 24 | profile: false, 25 | typescriptPath: require.resolve('typescript'), 26 | }; 27 | 28 | beforeEach(() => { 29 | compiler = { 30 | options: { 31 | context, 32 | }, 33 | } as webpack.Compiler; 34 | }); 35 | afterEach(() => { 36 | jest.resetModules(); 37 | }); 38 | 39 | it.each([ 40 | [undefined, configuration], 41 | [{}, configuration], 42 | [{ memoryLimit: 512 }, { ...configuration, memoryLimit: 512 }], 43 | [ 44 | { configFile: 'tsconfig.another.json' }, 45 | { 46 | ...configuration, 47 | configFile: path.normalize(path.resolve(context, 'tsconfig.another.json')), 48 | }, 49 | ], 50 | [{ build: true }, { ...configuration, build: true, mode: 'write-tsbuildinfo' }], 51 | [{ mode: 'readonly' }, { ...configuration, mode: 'readonly' }], 52 | [{ mode: 'write-tsbuildinfo' }, { ...configuration, mode: 'write-tsbuildinfo' }], 53 | [{ mode: 'write-dts' }, { ...configuration, mode: 'write-dts' }], 54 | [{ mode: 'write-references' }, { ...configuration, mode: 'write-references' }], 55 | [ 56 | { configOverwrite: { compilerOptions: { strict: true }, include: ['src'] } }, 57 | { 58 | ...configuration, 59 | configOverwrite: { 60 | compilerOptions: { 61 | strict: true, 62 | }, 63 | include: ['src'], 64 | }, 65 | }, 66 | ], 67 | [{ diagnosticOptions: {} }, configuration], 68 | [ 69 | { diagnosticOptions: { syntactic: true, semantic: false } }, 70 | { 71 | ...configuration, 72 | diagnosticOptions: { semantic: false, syntactic: true, declaration: false, global: false }, 73 | }, 74 | ], 75 | [{ profile: true }, { ...configuration, profile: true }], 76 | ])('creates configuration from options %p', async (options, expectedConfig) => { 77 | const { createTypeScriptWorkerConfig } = await import( 78 | 'src/typescript/type-script-worker-config' 79 | ); 80 | const config = createTypeScriptWorkerConfig(compiler, options as TypeScriptWorkerOptions); 81 | 82 | expect(config).toEqual(expectedConfig); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/unit/utils/async/pool.spec.ts: -------------------------------------------------------------------------------- 1 | import { createPool } from 'src/utils/async/pool'; 2 | 3 | function wait(timeout: number) { 4 | return new Promise((resolve) => setTimeout(resolve, timeout)); 5 | } 6 | 7 | describe('createPool', () => { 8 | it('creates new pool', () => { 9 | const pool = createPool(10); 10 | expect(pool).toBeDefined(); 11 | expect(pool.size).toEqual(10); 12 | expect(pool.pending).toEqual(0); 13 | expect(pool.submit).toBeInstanceOf(Function); 14 | }); 15 | 16 | it('limits concurrency', async () => { 17 | const pool = createPool(2); 18 | const shortTask = jest.fn(() => wait(10)); 19 | const longTask = jest.fn(() => wait(500)); 20 | 21 | pool.submit(shortTask); 22 | pool.submit(shortTask); 23 | pool.submit(longTask); 24 | pool.submit(longTask); 25 | pool.submit(longTask); 26 | 27 | expect(shortTask).toHaveBeenCalledTimes(2); 28 | expect(longTask).toHaveBeenCalledTimes(0); 29 | 30 | await wait(200); 31 | 32 | expect(shortTask).toHaveBeenCalledTimes(2); 33 | expect(longTask).toHaveBeenCalledTimes(2); 34 | 35 | await pool.drained; 36 | }); 37 | 38 | it('works after draining', async () => { 39 | const pool = createPool(2); 40 | const shortTask = jest.fn(() => wait(10)); 41 | 42 | pool.submit(shortTask); 43 | pool.submit(shortTask); 44 | pool.submit(shortTask); 45 | pool.submit(shortTask); 46 | 47 | expect(shortTask).toHaveBeenCalledTimes(2); 48 | 49 | await wait(100); 50 | 51 | expect(shortTask).toHaveBeenCalledTimes(4); 52 | 53 | pool.submit(shortTask); 54 | pool.submit(shortTask); 55 | 56 | expect(shortTask).toHaveBeenCalledTimes(6); 57 | 58 | await pool.drained; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/unit/utils/path/is-inside-another-path-unix.spec.ts: -------------------------------------------------------------------------------- 1 | import { isInsideAnotherPath } from '../../../../src/utils/path/is-inside-another-path'; 2 | 3 | jest.mock('path', () => jest.requireActual('path').posix); 4 | 5 | const unixTests: [string, string, boolean][] = [ 6 | // Identical 7 | ['/foo', '/foo', false], 8 | // Nothing in common 9 | ['/foo', '/bar', false], 10 | // subfolder 11 | ['/foo', '/foo/bar', true], 12 | // parallel 13 | ['/foo', '/foo/../bar', false], 14 | // relative subfolder 15 | ['/foo', '/foo/./bar', true], 16 | ]; 17 | 18 | describe('Properly detects ignored sub-folders on Unix', () => { 19 | it('should work on Unix', () => { 20 | unixTests.forEach(([parent, testedPath, expectedResult]) => { 21 | const result = isInsideAnotherPath(parent, testedPath); 22 | expect(result).toEqual(expectedResult); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/unit/utils/path/is-inside-another-path-windows.spec.ts: -------------------------------------------------------------------------------- 1 | import { isInsideAnotherPath } from '../../../../src/utils/path/is-inside-another-path'; 2 | 3 | jest.mock('path', () => jest.requireActual('path').win32); 4 | 5 | const windowsTests: [string, string, boolean][] = [ 6 | // subfolder 7 | ['C:\\Foo', 'C:\\Foo\\Bar', true], 8 | // Nothing in common 9 | ['C:\\Foo', 'C:\\Bar', false], 10 | // Wrong drive. 11 | ['C:\\Foo', 'D:\\Foo\\Bar', false], 12 | ]; 13 | 14 | describe('Properly detects ignored sub-folders on Windows', () => { 15 | it('should work on Windows', () => { 16 | windowsTests.forEach(([parent, testedPath, expectedResult]) => { 17 | const result = isInsideAnotherPath(parent, testedPath); 18 | expect(result).toEqual(expectedResult); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "lib": ["ES6"], 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "skipDefaultLibCheck": true, 11 | "strict": true, 12 | "declaration": true, 13 | "baseUrl": "./src", 14 | "outDir": "./lib", 15 | "forceConsistentCasingInFileNames": true 16 | }, 17 | "include": ["./src"], 18 | "exclude": ["node_modules", "test", "lib"] 19 | } 20 | --------------------------------------------------------------------------------