├── .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 |
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 |
--------------------------------------------------------------------------------