Skip to content

Commit

Permalink
feat: support @vue/compiler-sfc in the vue extension (#423)
Browse files Browse the repository at this point in the history
Closes: #384
  • Loading branch information
piotr-oles committed May 26, 2020
1 parent f277444 commit 8687c63
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 89 deletions.
75 changes: 65 additions & 10 deletions src/typescript-reporter/extension/vue/TypeScriptVueExtension.ts
Expand Up @@ -5,16 +5,44 @@ import {
import fs from 'fs-extra';
import { TypeScriptExtension } from '../TypeScriptExtension';
import { TypeScriptVueExtensionConfiguration } from './TypeScriptVueExtensionConfiguration';
import { VueTemplateCompiler } from './types/vue-template-compiler';
import { VueTemplateCompilerV2 } from './types/vue-template-compiler';
import { VueTemplateCompilerV3 } from './types/vue__compiler-sfc';

interface GenericScriptSFCBlock {
content: string;
attrs: Record<string, string | true>;
start?: number;
end?: number;
lang?: string;
src?: string;
}

function createTypeScriptVueExtension(
configuration: TypeScriptVueExtensionConfiguration
): TypeScriptExtension {
function loadVueCompiler(): VueTemplateCompiler {
function loadVueTemplateCompiler(): VueTemplateCompilerV2 | VueTemplateCompilerV3 {
return require(configuration.compiler);
}

function getExtensionByLang(lang: string | undefined): TypeScriptEmbeddedSource['extension'] {
function isVueTemplateCompilerV2(
compiler: VueTemplateCompilerV2 | VueTemplateCompilerV3
): compiler is VueTemplateCompilerV2 {
return typeof (compiler as VueTemplateCompilerV2).parseComponent === 'function';
}

function isVueTemplateCompilerV3(
compiler: VueTemplateCompilerV2 | VueTemplateCompilerV3
): compiler is VueTemplateCompilerV3 {
return typeof (compiler as VueTemplateCompilerV3).parse === 'function';
}

function getExtensionByLang(
lang: string | true | undefined
): TypeScriptEmbeddedSource['extension'] {
if (lang === true) {
return '.js';
}

switch (lang) {
case 'ts':
return '.ts';
Expand All @@ -36,7 +64,7 @@ function createTypeScriptVueExtension(

function createVueSrcScriptEmbeddedSource(
src: string,
lang: string | undefined
lang: string | true | undefined
): TypeScriptEmbeddedSource {
// Import path cannot be end with '.ts[x]'
src = src.replace(/\.tsx?$/i, '');
Expand All @@ -58,7 +86,7 @@ function createTypeScriptVueExtension(

function createVueInlineScriptEmbeddedSource(
text: string,
lang: string | undefined
lang: string | true | undefined
): TypeScriptEmbeddedSource {
return {
sourceText: text,
Expand All @@ -71,19 +99,46 @@ function createTypeScriptVueExtension(
return undefined;
}

const compiler = loadVueCompiler();
const compiler = loadVueTemplateCompiler();
const vueSourceText = fs.readFileSync(fileName, { encoding: 'utf-8' });

const { script } = compiler.parseComponent(vueSourceText, {
pad: 'space',
});
let script: GenericScriptSFCBlock | undefined;
if (isVueTemplateCompilerV2(compiler)) {
const parsed = compiler.parseComponent(vueSourceText, {
pad: 'space',
});

script = parsed.script;
} else if (isVueTemplateCompilerV3(compiler)) {
const parsed = compiler.parse(vueSourceText);

if (parsed.descriptor && parsed.descriptor.script) {
const scriptV3 = parsed.descriptor.script;

// map newer version of SFCScriptBlock to the generic one
script = {
content: scriptV3.content,
attrs: scriptV3.attrs,
start: scriptV3.loc.start.offset,
end: scriptV3.loc.end.offset,
lang: scriptV3.lang,
src: scriptV3.src,
};
}
} else {
throw new Error(
'Unsupported vue template compiler. Compiler should provide `parse` or `parseComponent` function.'
);
}

if (!script) {
// No <script> block
return createVueNoScriptEmbeddedSource();
} else if (script.attrs.src) {
// <script src="file.ts" /> block
return createVueSrcScriptEmbeddedSource(script.attrs.src, script.attrs.lang);
if (typeof script.attrs.src === 'string') {
return createVueSrcScriptEmbeddedSource(script.attrs.src, script.attrs.lang);
}
} else {
// <script lang="ts"></script> block
// pad blank lines to retain diagnostics location
Expand Down
Expand Up @@ -2,11 +2,11 @@
* This declaration is copied from https://github.com/vuejs/vue/pull/7918
* which may included vue-template-compiler v2.6.0.
*/
interface SFCParserOptions {
interface SFCParserOptionsV2 {
pad?: true | 'line' | 'space';
}

export interface SFCBlock {
export interface SFCBlockV2 {
type: string;
content: string;
attrs: Record<string, string>;
Expand All @@ -18,13 +18,13 @@ export interface SFCBlock {
module?: string | boolean;
}

export interface SFCDescriptor {
template: SFCBlock | undefined;
script: SFCBlock | undefined;
styles: SFCBlock[];
customBlocks: SFCBlock[];
export interface SFCDescriptorV2 {
template: SFCBlockV2 | undefined;
script: SFCBlockV2 | undefined;
styles: SFCBlockV2[];
customBlocks: SFCBlockV2[];
}

export interface VueTemplateCompiler {
parseComponent(file: string, options?: SFCParserOptions): SFCDescriptor;
export interface VueTemplateCompilerV2 {
parseComponent(file: string, options?: SFCParserOptionsV2): SFCDescriptorV2;
}
43 changes: 43 additions & 0 deletions src/typescript-reporter/extension/vue/types/vue__compiler-sfc.ts
@@ -0,0 +1,43 @@
interface Position {
offset: number;
line: number;
column: number;
}

interface SourceLocation {
start: Position;
end: Position;
source: string;
}

export interface SFCBlock {
type: string;
content: string;
attrs: Record<string, string | true>;
loc: SourceLocation;
lang?: string;
src?: string;
}

interface SFCDescriptor {
filename: string;
template: SFCBlock | null;
script: SFCBlock | null;
styles: SFCBlock[];
customBlocks: SFCBlock[];
}

interface CompilerError extends SyntaxError {
code: number;
loc?: SourceLocation;
}

interface SFCParseResult {
descriptor: SFCDescriptor;
errors: CompilerError[];
}

export interface VueTemplateCompilerV3 {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parse(template: string, options?: any): SFCParseResult;
}
96 changes: 75 additions & 21 deletions test/e2e/TypeScriptVueExtension.spec.ts
Expand Up @@ -22,9 +22,26 @@ describe('TypeScript Vue Extension', () => {
await sandbox.cleanup();
});

it.each([{ async: false, typescript: '^3.8.0', tsloader: '^7.0.0' }])(
it.each([
{
async: false,
typescript: '^3.8.0',
tsloader: '^7.0.0',
vueloader: '^15.8.3',
vue: '^2.6.11',
compiler: 'vue-template-compiler',
},
{
async: true,
typescript: '^3.8.0',
tsloader: '^7.0.0',
vueloader: 'v16.0.0-beta.3',
vue: '^3.0.0-beta.14',
compiler: '@vue/compiler-sfc',
},
])(
'reports semantic error for %p',
async ({ async, typescript, tsloader }) => {
async ({ async, typescript, tsloader, vueloader, vue, compiler }) => {
await sandbox.load([
await readFixture(join(__dirname, 'fixtures/environment/typescript-vue.fixture'), {
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
Expand All @@ -35,11 +52,48 @@ describe('TypeScript Vue Extension', () => {
WEBPACK_VERSION: JSON.stringify('^4.0.0'),
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
WEBPACK_DEV_SERVER_VERSION: JSON.stringify(WEBPACK_DEV_SERVER_VERSION),
VUE_LOADER_VERSION: JSON.stringify(vueloader),
VUE_VERSION: JSON.stringify(vue),
VUE_COMPILER: JSON.stringify(compiler),
ASYNC: JSON.stringify(async),
}),
await readFixture(join(__dirname, 'fixtures/implementation/typescript-vue.fixture')),
]);

if (vue === '^2.6.11') {
await sandbox.write(
'src/index.ts',
[
"import Vue from 'vue'",
"import App from './App.vue'",
'',
'new Vue({',
' render: h => h(App)',
"}).$mount('#app')",
].join('\n')
);
await sandbox.write(
'src/vue-shim.d.ts',
[
'declare module "*.vue" {',
' import Vue from "vue";',
' export default Vue;',
'}',
].join('\n')
);
} else {
await sandbox.write(
'src/index.ts',
[
"import { createApp } from 'vue'",
"import App from './App.vue'",
'',
"createApp(App).mount('#app')",
].join('\n')
);
await sandbox.write('src/vue-shim.d.ts', 'declare module "*.vue";');
}

const driver = createWebpackDevServerDriver(
sandbox.spawn('npm run webpack-dev-server'),
async
Expand All @@ -49,7 +103,7 @@ describe('TypeScript Vue Extension', () => {
// first compilation is successful
await driver.waitForNoErrors();

// let's modify user model file
// modify user model file
await sandbox.patch(
'src/component/LoggedIn.vue',
"import User, { getUserName } from '@/model/User';",
Expand All @@ -60,43 +114,43 @@ describe('TypeScript Vue Extension', () => {
errors = await driver.waitForErrors();
expect(errors).toEqual([
[
'ERROR in src/component/LoggedIn.vue 28:24-35',
'ERROR in src/component/LoggedIn.vue 27:21-32',
"TS2304: Cannot find name 'getUserName'.",
' 25 | const user: User = this.user;',
' 26 | ',
' 27 | get userName() {',
" > 28 | return this.user ? getUserName(this.user) : '';",
' | ^^^^^^^^^^^',
' 29 | }',
' 30 | ',
' 31 | async logout() {',
" > 27 | return user ? getUserName(user) : '';",
' | ^^^^^^^^^^^',
' 28 | }',
' 29 | },',
' 30 | async logout() {',
].join('\n'),
]);

// let's fix it
// fix it
await sandbox.patch(
'src/component/LoggedIn.vue',
"return this.user ? getUserName(this.user) : '';",
"return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';"
"return user ? getUserName(user) : '';",
"return user ? `${user.firstName} ${user.lastName}` : '';"
);

await driver.waitForNoErrors();

// let's modify user model file again
// modify user model file again
await sandbox.patch('src/model/User.ts', ' firstName?: string;\n', '');

// not we should have an error about missing firstName property
errors = await driver.waitForErrors();
expect(errors).toEqual([
[
'ERROR in src/component/LoggedIn.vue 28:37-46',
'ERROR in src/component/LoggedIn.vue 27:29-38',
"TS2339: Property 'firstName' does not exist on type 'User'.",
' 25 | const user: User = this.user;',
' 26 | ',
' 27 | get userName() {',
" > 28 | return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';",
' | ^^^^^^^^^',
' 29 | }',
' 30 | ',
' 31 | async logout() {',
" > 27 | return user ? `${user.firstName} ${user.lastName}` : '';",
' | ^^^^^^^^^',
' 28 | }',
' 29 | },',
' 30 | async logout() {',
].join('\n'),
[
'ERROR in src/model/User.ts 11:16-25',
Expand Down

0 comments on commit 8687c63

Please sign in to comment.