Skip to content

webpack side file watching #516

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 1 addition & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@ module.exports = {
};
```

If you are using **TypeScript >= 3.8.0**, it's recommended to:
* for `ts-loader` set `"importsNotUsedAsValues": "preserve"` [compiler option](https://www.typescriptlang.org/docs/handbook/compiler-options.html) in the [`tsconfig.json`](./examples/ts-loader/tsconfig.json)
* for `babel-loader` set `"onlyRemoveTypeImports": true` [preset option](https://babeljs.io/docs/en/babel-preset-typescript#onlyremovetypeimports) in the [babel configuration](./examples/babel-loader/.babelrc.js)

[Read more](#type-only-modules-watching) about type-only modules watching.

> Examples how to configure it with [babel-loader](https://github.com/babel/babel-loader), [ts-loader](https://github.com/TypeStrong/ts-loader),
> [eslint](https://github.com/eslint/eslint) and [Visual Studio Code](https://code.visualstudio.com/) are in the
> [**examples**](./examples) directory.
Expand Down Expand Up @@ -204,7 +198,7 @@ Options for the issues filtering (`issues` option object).
## Vue.js

⚠️ There are additional **constraints** regarding Vue.js Single File Component support: ⚠️
* It requires **TypeScript >= 3.8.0** and `"importsNotUsedAsValues": "preserve"` option in the `tsconfig.json` (it's a limitation of the `transpileOnly` mode from `ts-loader`)
* It requires **TypeScript >= 3.8.0** (it's a limitation of the `transpileOnly` mode from `ts-loader`)
* It doesn't work with the `build` mode (project references)

To enable Vue.js support, follow these steps:
Expand Down Expand Up @@ -314,17 +308,6 @@ declare module "*.vue" {

</details>

## Type-Only modules watching

At present `ts-loader` with `transpileOnly` mode and `babel-loader` will not add type-only files (files that contains only interfaces and/or types)
to the webpack dependencies set. Webpack watches only files that are in the dependencies set. This means that
changes in type-only files will **not** trigger new compilation and therefore type-checker in watch mode.

If you use **TypeScript >=3.8.0**, you can fix it:
* for `ts-loader` set `"importsNotUsedAsValues": "preserve"` [compiler option](https://www.typescriptlang.org/docs/handbook/compiler-options.html) in the [`tsconfig.json`](./examples/ts-loader/tsconfig.json)
* for `babel-loader` set `"onlyRemoveTypeImports": true` [preset option](https://babeljs.io/docs/en/babel-preset-typescript#onlyremovetypeimports) in the [babel configuration](./examples/babel-loader/.babelrc.js)


## Plugin hooks

This plugin provides some custom webpack hooks:
Expand Down
10 changes: 1 addition & 9 deletions examples/babel-loader/.babelrc.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
module.exports = {
presets: [
'@babel/preset-env',
[
'@babel/preset-typescript',
{
onlyRemoveTypeImports: true, // this is important for proper files watching
},
],
],
presets: ['@babel/preset-env', ['@babel/preset-typescript']],
};
3 changes: 1 addition & 2 deletions examples/ts-loader/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
"strict": true,
"baseUrl": "./src",
"outDir": "./dist",
"forceConsistentCasingInFileNames": true,
"importsNotUsedAsValues": "preserve" // this is important for proper files watching
"forceConsistentCasingInFileNames": true
},
"include": ["./src"],
"exclude": ["node_modules"]
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@babel/code-frame": "^7.8.3",
"@types/json-schema": "^7.0.5",
"chalk": "^4.1.0",
"chokidar": "^3.4.2",
"cosmiconfig": "^6.0.0",
"deepmerge": "^4.2.2",
"fs-extra": "^9.0.0",
Expand Down
6 changes: 3 additions & 3 deletions src/ForkTsCheckerWebpackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import { assertEsLintSupport } from './eslint-reporter/assertEsLintSupport';
import { createEsLintReporterRpcClient } from './eslint-reporter/reporter/EsLintReporterRpcClient';
import { tapStartToConnectAndRunReporter } from './hooks/tapStartToConnectAndRunReporter';
import { tapStopToDisconnectReporter } from './hooks/tapStopToDisconnectReporter';
import { tapDoneToCollectRemoved } from './hooks/tapDoneToCollectRemoved';
import { tapAfterCompileToAddDependencies } from './hooks/tapAfterCompileToAddDependencies';
import { tapErrorToLogMessage } from './hooks/tapErrorToLogMessage';
import { getForkTsCheckerWebpackPluginHooks } from './hooks/pluginHooks';
import { tapAfterEnvironmentToPatchWatching } from './hooks/tapAfterEnvironmentToPatchWatching';

class ForkTsCheckerWebpackPlugin implements webpack.Plugin {
static readonly version: string = '{{VERSION}}'; // will be replaced by the @semantic-release/exec
Expand Down Expand Up @@ -62,9 +62,9 @@ class ForkTsCheckerWebpackPlugin implements webpack.Plugin {
if (reporters.length) {
const reporter = createAggregatedReporter(composeReporterRpcClients(reporters));

tapAfterEnvironmentToPatchWatching(compiler, state);
tapStartToConnectAndRunReporter(compiler, reporter, configuration, state);
tapDoneToCollectRemoved(compiler, configuration, state);
tapAfterCompileToAddDependencies(compiler, configuration);
tapAfterCompileToAddDependencies(compiler, configuration, state);
tapStopToDisconnectReporter(compiler, reporter, state);
tapErrorToLogMessage(compiler, configuration);
} else {
Expand Down
5 changes: 0 additions & 5 deletions src/ForkTsCheckerWebpackPluginOptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,11 +261,6 @@
},
"exclude": {
"$ref": "#/definitions/IssuePredicateOption"
},
"scope": {
"type": "string",
"enum": ["all", "webpack"],
"description": "Defines issues scope to be reported. If 'webpack', reports errors only related to a given webpack compilation. Reports all errors otherwise."
}
}
},
Expand Down
15 changes: 10 additions & 5 deletions src/ForkTsCheckerWebpackPluginState.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { Report } from './reporter';
import { Tap } from 'tapable';
import { Dependencies, Report } from './reporter';
import { Issue } from './issue';

interface ForkTsCheckerWebpackPluginState {
report: Promise<Report | undefined>;
removedFiles: string[];
reportPromise: Promise<Report | undefined>;
issuesPromise: Promise<Issue[] | undefined>;
dependenciesPromise: Promise<Dependencies | undefined>;
lastDependencies: Dependencies | undefined;
watching: boolean;
initialized: boolean;
webpackDevServerDoneTap: Tap | undefined;
}

function createForkTsCheckerWebpackPluginState(): ForkTsCheckerWebpackPluginState {
return {
report: Promise.resolve([]),
removedFiles: [],
reportPromise: Promise.resolve(undefined),
issuesPromise: Promise.resolve(undefined),
dependenciesPromise: Promise.resolve(undefined),
lastDependencies: undefined,
watching: false,
initialized: false,
webpackDevServerDoneTap: undefined,
Expand Down
98 changes: 56 additions & 42 deletions src/eslint-reporter/reporter/EsLintReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,55 +16,69 @@ function createEsLintReporter(configuration: EsLintReporterConfiguration): Repor

return {
getReport: async ({ changedFiles = [], deletedFiles = [] }) => {
// cleanup old results
changedFiles.forEach((changedFile) => {
lintResults.delete(changedFile);
});
deletedFiles.forEach((removedFile) => {
lintResults.delete(removedFile);
});
return {
async getDependencies() {
return {
files: [],
dirs: [],
extensions: [],
};
},
async getIssues() {
// cleanup old results
changedFiles.forEach((changedFile) => {
lintResults.delete(changedFile);
});
deletedFiles.forEach((removedFile) => {
lintResults.delete(removedFile);
});

// get reports
const lintReports: LintReport[] = [];
// get reports
const lintReports: LintReport[] = [];

if (isInitialRun) {
lintReports.push(engine.executeOnFiles(includedFilesPatterns));
isInitialRun = false;
} else {
// we need to take care to not lint files that are not included by the configuration.
// the eslint engine will not exclude them automatically
const changedAndIncludedFiles = changedFiles.filter(
(changedFile) =>
includedFilesPatterns.some((includedFilesPattern) =>
minimatch(changedFile, includedFilesPattern)
) &&
(configuration.options.extensions || []).some((extension) =>
changedFile.endsWith(extension)
) &&
!engine.isPathIgnored(changedFile)
);
if (isInitialRun) {
lintReports.push(engine.executeOnFiles(includedFilesPatterns));
isInitialRun = false;
} else {
// we need to take care to not lint files that are not included by the configuration.
// the eslint engine will not exclude them automatically
const changedAndIncludedFiles = changedFiles.filter(
(changedFile) =>
includedFilesPatterns.some((includedFilesPattern) =>
minimatch(changedFile, includedFilesPattern)
) &&
(configuration.options.extensions || []).some((extension) =>
changedFile.endsWith(extension)
) &&
!engine.isPathIgnored(changedFile)
);

if (changedAndIncludedFiles.length) {
lintReports.push(engine.executeOnFiles(changedAndIncludedFiles));
}
}
if (changedAndIncludedFiles.length) {
lintReports.push(engine.executeOnFiles(changedAndIncludedFiles));
}
}

// output fixes if `fix` option is provided
if (configuration.options.fix) {
await Promise.all(lintReports.map((lintReport) => CLIEngine.outputFixes(lintReport)));
}
// output fixes if `fix` option is provided
if (configuration.options.fix) {
await Promise.all(lintReports.map((lintReport) => CLIEngine.outputFixes(lintReport)));
}

// store results
lintReports.forEach((lintReport) => {
lintReport.results.forEach((lintResult) => {
lintResults.set(lintResult.filePath, lintResult);
});
});
// store results
lintReports.forEach((lintReport) => {
lintReport.results.forEach((lintResult) => {
lintResults.set(lintResult.filePath, lintResult);
});
});

// get actual list of previous and current reports
const results = Array.from(lintResults.values());
// get actual list of previous and current reports
const results = Array.from(lintResults.values());

return createIssuesFromEsLintResults(results);
return createIssuesFromEsLintResults(results);
},
async close() {
// do nothing
},
};
},
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/eslint-reporter/reporter/EsLintReporterRpcClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as path from 'path';
import path from 'path';
import { EsLintReporterConfiguration } from '../EsLintReporterConfiguration';
import { createReporterRpcClient, ReporterRpcClient } from '../../reporter';
import { createRpcIpcMessageChannel } from '../../rpc/rpc-ipc';
Expand Down
20 changes: 7 additions & 13 deletions src/hooks/getChangedFiles.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import webpack from 'webpack';
import path from 'path';
import { getWatcher } from './getWatcher';
import { CompilerWithWatchFileSystem } from '../watch/CompilerWithWatchFileSystem';
import { InclusiveNodeWatchFileSystem } from '../watch/InclusiveNodeWatchFileSystem';

function getChangedFiles(compiler: webpack.Compiler): string[] {
let changedFiles: string[] = [];
const watchFileSystem = (compiler as CompilerWithWatchFileSystem<InclusiveNodeWatchFileSystem>)
.watchFileSystem;

if ((compiler as any).modifiedFiles) {
// webpack 5+
changedFiles = Array.from((compiler as any).modifiedFiles);
} else {
const watcher = getWatcher(compiler);
// webpack 4
changedFiles = Object.keys((watcher && watcher.mtimes) || {});
}

return changedFiles.map((changedFile) => path.normalize(changedFile));
return watchFileSystem
? Array.from(watchFileSystem.changedFiles).map((changedFile) => path.normalize(changedFile))
: [];
}

export { getChangedFiles };
24 changes: 8 additions & 16 deletions src/hooks/getDeletedFiles.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import webpack from 'webpack';
import path from 'path';
import { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState';
import { CompilerWithWatchFileSystem } from '../watch/CompilerWithWatchFileSystem';
import { InclusiveNodeWatchFileSystem } from '../watch/InclusiveNodeWatchFileSystem';

function getDeletedFiles(
compiler: webpack.Compiler,
state: ForkTsCheckerWebpackPluginState
): string[] {
let deletedFiles: string[] = [];
function getDeletedFiles(compiler: webpack.Compiler): string[] {
const watchFileSystem = (compiler as CompilerWithWatchFileSystem<InclusiveNodeWatchFileSystem>)
.watchFileSystem;

if ((compiler as any).removedFiles) {
// webpack 5+
deletedFiles = Array.from((compiler as any).removedFiles || []);
} else {
// webpack 4
deletedFiles = [...state.removedFiles];
}

return deletedFiles.map((changedFile) => path.normalize(changedFile));
return watchFileSystem
? Array.from(watchFileSystem.removedFiles).map((removeFile) => path.normalize(removeFile))
: [];
}

export { getDeletedFiles };
26 changes: 20 additions & 6 deletions src/hooks/tapAfterCompileToAddDependencies.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import webpack from 'webpack';
import path from 'path';
import { ForkTsCheckerWebpackPluginConfiguration } from '../ForkTsCheckerWebpackPluginConfiguration';
import { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState';

function tapAfterCompileToAddDependencies(
compiler: webpack.Compiler,
configuration: ForkTsCheckerWebpackPluginConfiguration
configuration: ForkTsCheckerWebpackPluginConfiguration,
state: ForkTsCheckerWebpackPluginState
) {
compiler.hooks.afterCompile.tap('ForkTsCheckerWebpackPlugin', (compilation) => {
if (configuration.typescript.enabled) {
// watch tsconfig.json file
compilation.fileDependencies.add(path.normalize(configuration.typescript.configFile));
compiler.hooks.afterCompile.tapPromise('ForkTsCheckerWebpackPlugin', async (compilation) => {
if (compilation.compiler !== compiler) {
// run only for the compiler that the plugin was registered for
return;
}

const dependencies = await state.dependenciesPromise;

if (dependencies) {
state.lastDependencies = dependencies;

dependencies.files.forEach((file) => {
compilation.fileDependencies.add(file);
});
dependencies.dirs.forEach((dir) => {
compilation.contextDependencies.add(dir);
});
}
});
}
Expand Down
9 changes: 1 addition & 8 deletions src/hooks/tapAfterCompileToGetIssues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function tapAfterCompileToGetIssues(
let issues: Issue[] | undefined = [];

try {
issues = await state.report;
issues = await state.issuesPromise;
} catch (error) {
hooks.error.call(error, compilation);
return;
Expand All @@ -33,13 +33,6 @@ function tapAfterCompileToGetIssues(
return;
}

if (configuration.issue.scope === 'webpack') {
// exclude issues that are related to files outside webpack compilation
issues = issues.filter(
(issue) => !issue.file || compilation.fileDependencies.has(path.normalize(issue.file))
);
}

// filter list of issues by provided issue predicate
issues = issues.filter(configuration.issue.predicate);

Expand Down
22 changes: 22 additions & 0 deletions src/hooks/tapAfterEnvironmentToPatchWatching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import webpack from 'webpack';
import { ForkTsCheckerWebpackPluginState } from '../ForkTsCheckerWebpackPluginState';
import { InclusiveNodeWatchFileSystem } from '../watch/InclusiveNodeWatchFileSystem';
import { CompilerWithWatchFileSystem } from '../watch/CompilerWithWatchFileSystem';

function tapAfterEnvironmentToPatchWatching(
compiler: webpack.Compiler,
state: ForkTsCheckerWebpackPluginState
) {
compiler.hooks.afterEnvironment.tap('ForkTsCheckerWebpackPlugin', () => {
const watchFileSystem = (compiler as CompilerWithWatchFileSystem).watchFileSystem;
if (watchFileSystem) {
// wrap original watch file system
(compiler as CompilerWithWatchFileSystem).watchFileSystem = new InclusiveNodeWatchFileSystem(
watchFileSystem,
state
);
}
});
}

export { tapAfterEnvironmentToPatchWatching };
Loading