Skip to content
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

Adds .vue functionality #77

Merged
merged 11 commits into from
Jan 9, 2018
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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need css-loader here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, css-loader is for the Vue integration test. Since Vue uses single-file-components, I wanted to include a simple css section in the example files just to ensure everything is working. Also, I added an import exampleCss from "./example.css" to ensure other "non-vue" imports work (as reported by @aweiu @CKGrafico @jvbianchi).

"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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, add information about vue support and how it resolves "@/*" paths in the README.md :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added vue section to README. 😄

* 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