Skip to content

Commit

Permalink
Merge pull request #77 from prograhammer/feature/vue
Browse files Browse the repository at this point in the history
Adds .vue functionality
  • Loading branch information
piotr-oles authored Jan 9, 2018
2 parents 9b0c9c9 + 57f2320 commit 48055c6
Show file tree
Hide file tree
Showing 17 changed files with 590 additions and 11 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ jspm_packages

# Optional npm cache directory
.npm
package-lock.json

# Optional REPL history
.node_repl_history

# IDEA directory
# Editor directories and files
.idea
.vscode
93 changes: 93 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ should keep free 1 core for *build* and 1 core for a *system* *(for example syst
node doesn't share memory between workers - keep in mind that memory usage will increase. Be aware that in some scenarios increasing workers
number **can increase checking time**. Default: `ForkTsCheckerWebpackPlugin.ONE_CPU`.

* **vue** `boolean`:
If `true`, the linter and compiler will process VueJs single-file-component (.vue) files. See the
[Vue section](https://github.com/Realytics/fork-ts-checker-webpack-plugin#vue) further down for information on how to correctly setup your project.

### Pre-computed consts:
* `ForkTsCheckerWebpackPlugin.ONE_CPU` - always use one CPU
* `ForkTsCheckerWebpackPlugin.ALL_CPUS` - always use all CPUs (will increase build time)
Expand Down Expand Up @@ -141,5 +145,94 @@ This plugin provides some custom webpack hooks (all are sync):
|`fork-ts-checker-emit`| Service will add errors and warnings to webpack compilation ('build' mode) | `diagnostics`, `lints`, `elapsed` |
|`fork-ts-checker-done`| Service finished type checking and webpack finished compilation ('watch' mode) | `diagnostics`, `lints`, `elapsed` |

## Vue
1. Turn on the vue option in the plugin in your webpack config:

```
new ForkTsCheckerWebpackPlugin({
tslint: true,
vue: true
})
```

2. To activate TypeScript in your `.vue` files, you need to ensure your script tag's language attribute is set
to `ts` or `tsx` (also make sure you include the `.vue` extension in all your import statements as shown below):

```html
<script lang="ts">
import Hello from '@/components/hello.vue'
// ...
</script>
```

3. Ideally you are also using `ts-loader` (in transpileOnly mode). Your Webpack config rules may look something like this:

```
{
test: /\.ts$/,
loader: 'ts-loader',
include: [resolve('src'), resolve('test')],
options: {
appendTsSuffixTo: [/\.vue$/],
transpileOnly: true
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
```
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:

```json
{
"defaultSeverity": "error",
"extends": [
"tslint-config-standard"
]
}
```
5. Ensure your `tsconfig.json` includes .vue files:

```
// tsconfig.json
{
"include": [
"src/**/*.ts",
"src/**/*.vue"
],
"exclude": [
"node_modules"
]
}
```

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
the fallback for the `@` wildcard will be `[tsconfig directory]/src` (we hope to make this more flexible on future releases):
```
// tsconfig.json
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}
// In a .ts or .vue file...
import Hello from '@/components/hello.vue'
```

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.

## License
MIT
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@
"@types/lodash.startswith": "^4.2.3",
"@types/minimatch": "^3.0.1",
"@types/node": "^8.0.26",
"@types/resolve": "0.0.4",
"@types/webpack": "^3.0.10",
"chai": "^3.5.0",
"css-loader": "^0.28.7",
"eslint": "^3.19.0",
"istanbul": "^0.4.5",
"mocha": "^3.4.1",
Expand All @@ -62,6 +64,10 @@
"ts-loader": "^2.1.0",
"tslint": "^5.0.0",
"typescript": "^2.1.0",
"vue": "^2.5.9",
"vue-class-component": "^6.1.1",
"vue-loader": "^13.5.0",
"vue-template-compiler": "^2.5.9",
"webpack": "^3.0.0"
},
"peerDependencies": {
Expand All @@ -76,6 +82,8 @@
"lodash.isfunction": "^3.0.8",
"lodash.isstring": "^4.0.1",
"lodash.startswith": "^4.2.1",
"minimatch": "^3.0.4"
"minimatch": "^3.0.4",
"resolve": "^1.5.0",
"vue-parser": "^1.1.5"
}
}
35 changes: 28 additions & 7 deletions src/IncrementalChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import WorkSet = require('./WorkSet');
import NormalizedMessage = require('./NormalizedMessage');
import CancellationToken = require('./CancellationToken');
import minimatch = require('minimatch');
import VueProgram = require('./VueProgram');

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

vue: boolean;

constructor(
programConfigFile: string,
linterConfigFile: string | false,
watchPaths: string[],
workNumber: number,
workDivision: number,
checkSyntacticErrors: boolean
checkSyntacticErrors: boolean,
vue: boolean
) {
this.programConfigFile = programConfigFile;
this.linterConfigFile = linterConfigFile;
this.watchPaths = watchPaths;
this.workNumber = workNumber || 0;
this.workDivision = workDivision || 1;
this.checkSyntacticErrors = checkSyntacticErrors || false;
this.vue = vue || false;
// Use empty array of exclusions in general to avoid having
// to check of its existence later on.
this.linterExclusions = [];
Expand Down Expand Up @@ -130,7 +135,8 @@ class IncrementalChecker {

nextIteration() {
if (!this.watcher) {
this.watcher = new FilesWatcher(this.watchPaths, ['.ts', '.tsx']);
const watchExtensions = this.vue ? ['.ts', '.tsx', '.vue'] : ['.ts', '.tsx'];
this.watcher = new FilesWatcher(this.watchPaths, watchExtensions);

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

if (!this.programConfig) {
this.programConfig = IncrementalChecker.loadProgramConfig(this.programConfigFile);
}

if (!this.linterConfig && this.linterConfigFile) {
this.linterConfig = IncrementalChecker.loadLinterConfig(this.linterConfigFile);

Expand All @@ -158,12 +160,31 @@ class IncrementalChecker {
}
}

this.program = IncrementalChecker.createProgram(this.programConfig, this.files, this.watcher, this.program);
this.program = this.vue ? this.loadVueProgram() : this.loadDefaultProgram();

if (this.linterConfig) {
this.linter = IncrementalChecker.createLinter(this.program);
}
}

loadVueProgram() {
this.programConfig = this.programConfig || VueProgram.loadProgramConfig(this.programConfigFile);

return VueProgram.createProgram(
this.programConfig,
path.dirname(this.programConfigFile),
this.files,
this.watcher,
this.program
);
}

loadDefaultProgram() {
this.programConfig = this.programConfig || IncrementalChecker.loadProgramConfig(this.programConfigFile);

return IncrementalChecker.createProgram(this.programConfig, this.files, this.watcher, this.program);
}

hasLinter() {
return this.linter !== undefined;
}
Expand Down
145 changes: 145 additions & 0 deletions src/VueProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import fs = require('fs');
import path = require('path');
import ts = require('typescript');
import FilesRegister = require('./FilesRegister');
import FilesWatcher = require('./FilesWatcher');
import vueParser = require('vue-parser');

class VueProgram {
static loadProgramConfig(configFile: string) {
const extraExtensions = ['vue'];

const parseConfigHost: ts.ParseConfigHost = {
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
readDirectory: (rootDir, extensions, excludes, includes, depth) => {
return ts.sys.readDirectory(rootDir, extensions.concat(extraExtensions), excludes, includes, depth);
}
};

const parsed = ts.parseJsonConfigFileContent(
// Regardless of the setting in the tsconfig.json we want isolatedModules to be false
Object.assign(ts.readConfigFile(configFile, ts.sys.readFile).config, { isolatedModules: false }),
parseConfigHost,
path.dirname(configFile)
);

parsed.options.allowNonTsExtensions = true;

return parsed;
}

/**
* Since 99.9% of Vue projects use the wildcard '@/*', we only search for that in tsconfig CompilerOptions.paths.
* The path is resolved with thie given substitution and includes the CompilerOptions.baseUrl (if given).
* If no paths given in tsconfig, then the default substitution is '[tsconfig directory]/src'.
* (This is a fast, simplified inspiration of what's described here: https://github.com/Microsoft/TypeScript/issues/5039)
*/
public static resolveNonTsModuleName(moduleName: string, containingFile: string, basedir: string, options: ts.CompilerOptions) {
const baseUrl = options.baseUrl ? options.baseUrl : basedir;
const pattern = options.paths ? options.paths['@/*'] : undefined;
const substitution = pattern ? options.paths['@/*'][0].replace('*', '') : 'src';
const isWildcard = moduleName.substr(0, 2) === '@/';
const isRelative = !path.isAbsolute(moduleName);

if (isWildcard) {
moduleName = path.resolve(baseUrl, substitution, moduleName.substr(2));
} else if (isRelative) {
moduleName = path.resolve(path.dirname(containingFile), moduleName);
}

return moduleName;
}

public static isVue(filePath: string) {
return path.extname(filePath) === '.vue';
}

static createProgram(
programConfig: ts.ParsedCommandLine,
basedir: string,
files: FilesRegister,
watcher: FilesWatcher,
oldProgram: ts.Program
) {
const host = ts.createCompilerHost(programConfig.options);
const realGetSourceFile = host.getSourceFile;

// We need a host that can parse Vue SFCs (single file components).
host.getSourceFile = (filePath, languageVersion, onError) => {
// first check if watcher is watching file - if not - check it's mtime
if (!watcher.isWatchingFile(filePath)) {
try {
const stats = fs.statSync(filePath);

files.setMtime(filePath, stats.mtime.valueOf());
} catch (e) {
// probably file does not exists
files.remove(filePath);
}
}

// get source file only if there is no source in files register
if (!files.has(filePath) || !files.getData(filePath).source) {
files.mutateData(filePath, (data) => {
data.source = realGetSourceFile(filePath, languageVersion, onError);
});
}

let source = files.getData(filePath).source;

// get typescript contents from Vue file
if (source && VueProgram.isVue(filePath)) {
const parsed = vueParser.parse(source.text, 'script', { lang: ['ts', 'tsx', 'js', 'jsx'] });
source = ts.createSourceFile(filePath, parsed, languageVersion, true);
}

return source;
};

// We need a host with special module resolution for Vue files.
host.resolveModuleNames = (moduleNames, containingFile) => {
const resolvedModules: ts.ResolvedModule[] = [];

for (const moduleName of moduleNames) {
// Try to use standard resolution.
const result = ts.resolveModuleName(moduleName, containingFile, programConfig.options, {
fileExists: host.fileExists,
readFile: host.readFile
});

if (result.resolvedModule) {
resolvedModules.push(result.resolvedModule);
} else {
// For non-ts extensions.
const absolutePath = VueProgram.resolveNonTsModuleName(moduleName, containingFile, basedir, programConfig.options);

if (VueProgram.isVue(moduleName)) {
resolvedModules.push({
resolvedFileName: absolutePath,
extension: '.ts'
} as ts.ResolvedModuleFull);
} else {
resolvedModules.push({
// If the file does exist, return an empty string (because we assume user has provided a ".d.ts" file for it).
resolvedFileName: host.fileExists(absolutePath) ? '' : absolutePath,
extension: '.ts'
} as ts.ResolvedModuleFull);
}
}
}

return resolvedModules;
};

return ts.createProgram(
programConfig.fileNames,
programConfig.options,
host,
oldProgram // re-use old program
);
}
}

export = VueProgram;
Loading

0 comments on commit 48055c6

Please sign in to comment.