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

feat: support @vue/compiler-sfc in the vue extension #423

Merged
merged 1 commit into from
May 26, 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
75 changes: 65 additions & 10 deletions src/typescript-reporter/extension/vue/TypeScriptVueExtension.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading