Skip to content

Commit 48055c6

Browse files
authored
Merge pull request #77 from prograhammer/feature/vue
Adds .vue functionality
2 parents 9b0c9c9 + 57f2320 commit 48055c6

17 files changed

+590
-11
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ jspm_packages
1717

1818
# Optional npm cache directory
1919
.npm
20+
package-lock.json
2021

2122
# Optional REPL history
2223
.node_repl_history
2324

24-
# IDEA directory
25+
# Editor directories and files
2526
.idea
27+
.vscode

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ should keep free 1 core for *build* and 1 core for a *system* *(for example syst
104104
node doesn't share memory between workers - keep in mind that memory usage will increase. Be aware that in some scenarios increasing workers
105105
number **can increase checking time**. Default: `ForkTsCheckerWebpackPlugin.ONE_CPU`.
106106

107+
* **vue** `boolean`:
108+
If `true`, the linter and compiler will process VueJs single-file-component (.vue) files. See the
109+
[Vue section](https://github.com/Realytics/fork-ts-checker-webpack-plugin#vue) further down for information on how to correctly setup your project.
110+
107111
### Pre-computed consts:
108112
* `ForkTsCheckerWebpackPlugin.ONE_CPU` - always use one CPU
109113
* `ForkTsCheckerWebpackPlugin.ALL_CPUS` - always use all CPUs (will increase build time)
@@ -141,5 +145,94 @@ This plugin provides some custom webpack hooks (all are sync):
141145
|`fork-ts-checker-emit`| Service will add errors and warnings to webpack compilation ('build' mode) | `diagnostics`, `lints`, `elapsed` |
142146
|`fork-ts-checker-done`| Service finished type checking and webpack finished compilation ('watch' mode) | `diagnostics`, `lints`, `elapsed` |
143147

148+
## Vue
149+
1. Turn on the vue option in the plugin in your webpack config:
150+
151+
```
152+
new ForkTsCheckerWebpackPlugin({
153+
tslint: true,
154+
vue: true
155+
})
156+
```
157+
158+
2. To activate TypeScript in your `.vue` files, you need to ensure your script tag's language attribute is set
159+
to `ts` or `tsx` (also make sure you include the `.vue` extension in all your import statements as shown below):
160+
161+
```html
162+
<script lang="ts">
163+
import Hello from '@/components/hello.vue'
164+
165+
// ...
166+
167+
</script>
168+
```
169+
170+
3. Ideally you are also using `ts-loader` (in transpileOnly mode). Your Webpack config rules may look something like this:
171+
172+
```
173+
{
174+
test: /\.ts$/,
175+
loader: 'ts-loader',
176+
include: [resolve('src'), resolve('test')],
177+
options: {
178+
appendTsSuffixTo: [/\.vue$/],
179+
transpileOnly: true
180+
}
181+
},
182+
{
183+
test: /\.vue$/,
184+
loader: 'vue-loader',
185+
options: vueLoaderConfig
186+
},
187+
```
188+
4. Add rules to your `tslint.json` and they will be applied to Vue files. For example, you could apply the Standard JS rules [tslint-config-standard](https://github.com/blakeembrey/tslint-config-standard) like this:
189+
190+
```json
191+
{
192+
"defaultSeverity": "error",
193+
"extends": [
194+
"tslint-config-standard"
195+
]
196+
}
197+
```
198+
5. Ensure your `tsconfig.json` includes .vue files:
199+
200+
```
201+
// tsconfig.json
202+
{
203+
"include": [
204+
"src/**/*.ts",
205+
"src/**/*.vue"
206+
],
207+
"exclude": [
208+
"node_modules"
209+
]
210+
}
211+
```
212+
213+
6. The commonly used `@` path wildcard will work if you set up a `baseUrl` and `paths` (in `compilerOptions`) to include `@/*`. If you don't set this, then
214+
the fallback for the `@` wildcard will be `[tsconfig directory]/src` (we hope to make this more flexible on future releases):
215+
```
216+
// tsconfig.json
217+
{
218+
"compilerOptions": {
219+
220+
// ...
221+
222+
"baseUrl": ".",
223+
"paths": {
224+
"@/*": [
225+
"src/*"
226+
]
227+
}
228+
}
229+
}
230+
231+
// In a .ts or .vue file...
232+
import Hello from '@/components/hello.vue'
233+
```
234+
235+
7. If you are working in **VSCode**, you can get extensions [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) and [TSLint Vue](https://marketplace.visualstudio.com/items?itemName=prograhammer.tslint-vue) to complete the developer workflow.
236+
144237
## License
145238
MIT

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@
5050
"@types/lodash.startswith": "^4.2.3",
5151
"@types/minimatch": "^3.0.1",
5252
"@types/node": "^8.0.26",
53+
"@types/resolve": "0.0.4",
5354
"@types/webpack": "^3.0.10",
5455
"chai": "^3.5.0",
56+
"css-loader": "^0.28.7",
5557
"eslint": "^3.19.0",
5658
"istanbul": "^0.4.5",
5759
"mocha": "^3.4.1",
@@ -62,6 +64,10 @@
6264
"ts-loader": "^2.1.0",
6365
"tslint": "^5.0.0",
6466
"typescript": "^2.1.0",
67+
"vue": "^2.5.9",
68+
"vue-class-component": "^6.1.1",
69+
"vue-loader": "^13.5.0",
70+
"vue-template-compiler": "^2.5.9",
6571
"webpack": "^3.0.0"
6672
},
6773
"peerDependencies": {
@@ -76,6 +82,8 @@
7682
"lodash.isfunction": "^3.0.8",
7783
"lodash.isstring": "^4.0.1",
7884
"lodash.startswith": "^4.2.1",
79-
"minimatch": "^3.0.4"
85+
"minimatch": "^3.0.4",
86+
"resolve": "^1.5.0",
87+
"vue-parser": "^1.1.5"
8088
}
8189
}

src/IncrementalChecker.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import WorkSet = require('./WorkSet');
99
import NormalizedMessage = require('./NormalizedMessage');
1010
import CancellationToken = require('./CancellationToken');
1111
import minimatch = require('minimatch');
12+
import VueProgram = require('./VueProgram');
1213

1314
// Need some augmentation here - linterOptions.exclude is not (yet) part of the official
1415
// types for tslint.
@@ -36,20 +37,24 @@ class IncrementalChecker {
3637
programConfig: ts.ParsedCommandLine;
3738
watcher: FilesWatcher;
3839

40+
vue: boolean;
41+
3942
constructor(
4043
programConfigFile: string,
4144
linterConfigFile: string | false,
4245
watchPaths: string[],
4346
workNumber: number,
4447
workDivision: number,
45-
checkSyntacticErrors: boolean
48+
checkSyntacticErrors: boolean,
49+
vue: boolean
4650
) {
4751
this.programConfigFile = programConfigFile;
4852
this.linterConfigFile = linterConfigFile;
4953
this.watchPaths = watchPaths;
5054
this.workNumber = workNumber || 0;
5155
this.workDivision = workDivision || 1;
5256
this.checkSyntacticErrors = checkSyntacticErrors || false;
57+
this.vue = vue || false;
5358
// Use empty array of exclusions in general to avoid having
5459
// to check of its existence later on.
5560
this.linterExclusions = [];
@@ -130,7 +135,8 @@ class IncrementalChecker {
130135

131136
nextIteration() {
132137
if (!this.watcher) {
133-
this.watcher = new FilesWatcher(this.watchPaths, ['.ts', '.tsx']);
138+
const watchExtensions = this.vue ? ['.ts', '.tsx', '.vue'] : ['.ts', '.tsx'];
139+
this.watcher = new FilesWatcher(this.watchPaths, watchExtensions);
134140

135141
// connect watcher with register
136142
this.watcher.on('change', (filePath: string, stats: fs.Stats) => {
@@ -143,10 +149,6 @@ class IncrementalChecker {
143149
this.watcher.watch();
144150
}
145151

146-
if (!this.programConfig) {
147-
this.programConfig = IncrementalChecker.loadProgramConfig(this.programConfigFile);
148-
}
149-
150152
if (!this.linterConfig && this.linterConfigFile) {
151153
this.linterConfig = IncrementalChecker.loadLinterConfig(this.linterConfigFile);
152154

@@ -158,12 +160,31 @@ class IncrementalChecker {
158160
}
159161
}
160162

161-
this.program = IncrementalChecker.createProgram(this.programConfig, this.files, this.watcher, this.program);
163+
this.program = this.vue ? this.loadVueProgram() : this.loadDefaultProgram();
164+
162165
if (this.linterConfig) {
163166
this.linter = IncrementalChecker.createLinter(this.program);
164167
}
165168
}
166169

170+
loadVueProgram() {
171+
this.programConfig = this.programConfig || VueProgram.loadProgramConfig(this.programConfigFile);
172+
173+
return VueProgram.createProgram(
174+
this.programConfig,
175+
path.dirname(this.programConfigFile),
176+
this.files,
177+
this.watcher,
178+
this.program
179+
);
180+
}
181+
182+
loadDefaultProgram() {
183+
this.programConfig = this.programConfig || IncrementalChecker.loadProgramConfig(this.programConfigFile);
184+
185+
return IncrementalChecker.createProgram(this.programConfig, this.files, this.watcher, this.program);
186+
}
187+
167188
hasLinter() {
168189
return this.linter !== undefined;
169190
}

src/VueProgram.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import fs = require('fs');
2+
import path = require('path');
3+
import ts = require('typescript');
4+
import FilesRegister = require('./FilesRegister');
5+
import FilesWatcher = require('./FilesWatcher');
6+
import vueParser = require('vue-parser');
7+
8+
class VueProgram {
9+
static loadProgramConfig(configFile: string) {
10+
const extraExtensions = ['vue'];
11+
12+
const parseConfigHost: ts.ParseConfigHost = {
13+
fileExists: ts.sys.fileExists,
14+
readFile: ts.sys.readFile,
15+
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
16+
readDirectory: (rootDir, extensions, excludes, includes, depth) => {
17+
return ts.sys.readDirectory(rootDir, extensions.concat(extraExtensions), excludes, includes, depth);
18+
}
19+
};
20+
21+
const parsed = ts.parseJsonConfigFileContent(
22+
// Regardless of the setting in the tsconfig.json we want isolatedModules to be false
23+
Object.assign(ts.readConfigFile(configFile, ts.sys.readFile).config, { isolatedModules: false }),
24+
parseConfigHost,
25+
path.dirname(configFile)
26+
);
27+
28+
parsed.options.allowNonTsExtensions = true;
29+
30+
return parsed;
31+
}
32+
33+
/**
34+
* Since 99.9% of Vue projects use the wildcard '@/*', we only search for that in tsconfig CompilerOptions.paths.
35+
* The path is resolved with thie given substitution and includes the CompilerOptions.baseUrl (if given).
36+
* If no paths given in tsconfig, then the default substitution is '[tsconfig directory]/src'.
37+
* (This is a fast, simplified inspiration of what's described here: https://github.com/Microsoft/TypeScript/issues/5039)
38+
*/
39+
public static resolveNonTsModuleName(moduleName: string, containingFile: string, basedir: string, options: ts.CompilerOptions) {
40+
const baseUrl = options.baseUrl ? options.baseUrl : basedir;
41+
const pattern = options.paths ? options.paths['@/*'] : undefined;
42+
const substitution = pattern ? options.paths['@/*'][0].replace('*', '') : 'src';
43+
const isWildcard = moduleName.substr(0, 2) === '@/';
44+
const isRelative = !path.isAbsolute(moduleName);
45+
46+
if (isWildcard) {
47+
moduleName = path.resolve(baseUrl, substitution, moduleName.substr(2));
48+
} else if (isRelative) {
49+
moduleName = path.resolve(path.dirname(containingFile), moduleName);
50+
}
51+
52+
return moduleName;
53+
}
54+
55+
public static isVue(filePath: string) {
56+
return path.extname(filePath) === '.vue';
57+
}
58+
59+
static createProgram(
60+
programConfig: ts.ParsedCommandLine,
61+
basedir: string,
62+
files: FilesRegister,
63+
watcher: FilesWatcher,
64+
oldProgram: ts.Program
65+
) {
66+
const host = ts.createCompilerHost(programConfig.options);
67+
const realGetSourceFile = host.getSourceFile;
68+
69+
// We need a host that can parse Vue SFCs (single file components).
70+
host.getSourceFile = (filePath, languageVersion, onError) => {
71+
// first check if watcher is watching file - if not - check it's mtime
72+
if (!watcher.isWatchingFile(filePath)) {
73+
try {
74+
const stats = fs.statSync(filePath);
75+
76+
files.setMtime(filePath, stats.mtime.valueOf());
77+
} catch (e) {
78+
// probably file does not exists
79+
files.remove(filePath);
80+
}
81+
}
82+
83+
// get source file only if there is no source in files register
84+
if (!files.has(filePath) || !files.getData(filePath).source) {
85+
files.mutateData(filePath, (data) => {
86+
data.source = realGetSourceFile(filePath, languageVersion, onError);
87+
});
88+
}
89+
90+
let source = files.getData(filePath).source;
91+
92+
// get typescript contents from Vue file
93+
if (source && VueProgram.isVue(filePath)) {
94+
const parsed = vueParser.parse(source.text, 'script', { lang: ['ts', 'tsx', 'js', 'jsx'] });
95+
source = ts.createSourceFile(filePath, parsed, languageVersion, true);
96+
}
97+
98+
return source;
99+
};
100+
101+
// We need a host with special module resolution for Vue files.
102+
host.resolveModuleNames = (moduleNames, containingFile) => {
103+
const resolvedModules: ts.ResolvedModule[] = [];
104+
105+
for (const moduleName of moduleNames) {
106+
// Try to use standard resolution.
107+
const result = ts.resolveModuleName(moduleName, containingFile, programConfig.options, {
108+
fileExists: host.fileExists,
109+
readFile: host.readFile
110+
});
111+
112+
if (result.resolvedModule) {
113+
resolvedModules.push(result.resolvedModule);
114+
} else {
115+
// For non-ts extensions.
116+
const absolutePath = VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, programConfig.options);
117+
118+
if (VueProgram.isVue(moduleName)) {
119+
resolvedModules.push({
120+
resolvedFileName: absolutePath,
121+
extension: '.ts'
122+
} as ts.ResolvedModuleFull);
123+
} else {
124+
resolvedModules.push({
125+
// If the file does exist, return an empty string (because we assume user has provided a ".d.ts" file for it).
126+
resolvedFileName: host.fileExists(absolutePath) ? '' : absolutePath,
127+
extension: '.ts'
128+
} as ts.ResolvedModuleFull);
129+
}
130+
}
131+
}
132+
133+
return resolvedModules;
134+
};
135+
136+
return ts.createProgram(
137+
programConfig.fileNames,
138+
programConfig.options,
139+
host,
140+
oldProgram // re-use old program
141+
);
142+
}
143+
}
144+
145+
export = VueProgram;

0 commit comments

Comments
 (0)