Skip to content

Commit 8687c63

Browse files
authored
feat: support @vue/compiler-sfc in the vue extension (#423)
Closes: #384
1 parent f277444 commit 8687c63

File tree

7 files changed

+246
-89
lines changed

7 files changed

+246
-89
lines changed

src/typescript-reporter/extension/vue/TypeScriptVueExtension.ts

+65-10
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,44 @@ import {
55
import fs from 'fs-extra';
66
import { TypeScriptExtension } from '../TypeScriptExtension';
77
import { TypeScriptVueExtensionConfiguration } from './TypeScriptVueExtensionConfiguration';
8-
import { VueTemplateCompiler } from './types/vue-template-compiler';
8+
import { VueTemplateCompilerV2 } from './types/vue-template-compiler';
9+
import { VueTemplateCompilerV3 } from './types/vue__compiler-sfc';
10+
11+
interface GenericScriptSFCBlock {
12+
content: string;
13+
attrs: Record<string, string | true>;
14+
start?: number;
15+
end?: number;
16+
lang?: string;
17+
src?: string;
18+
}
919

1020
function createTypeScriptVueExtension(
1121
configuration: TypeScriptVueExtensionConfiguration
1222
): TypeScriptExtension {
13-
function loadVueCompiler(): VueTemplateCompiler {
23+
function loadVueTemplateCompiler(): VueTemplateCompilerV2 | VueTemplateCompilerV3 {
1424
return require(configuration.compiler);
1525
}
1626

17-
function getExtensionByLang(lang: string | undefined): TypeScriptEmbeddedSource['extension'] {
27+
function isVueTemplateCompilerV2(
28+
compiler: VueTemplateCompilerV2 | VueTemplateCompilerV3
29+
): compiler is VueTemplateCompilerV2 {
30+
return typeof (compiler as VueTemplateCompilerV2).parseComponent === 'function';
31+
}
32+
33+
function isVueTemplateCompilerV3(
34+
compiler: VueTemplateCompilerV2 | VueTemplateCompilerV3
35+
): compiler is VueTemplateCompilerV3 {
36+
return typeof (compiler as VueTemplateCompilerV3).parse === 'function';
37+
}
38+
39+
function getExtensionByLang(
40+
lang: string | true | undefined
41+
): TypeScriptEmbeddedSource['extension'] {
42+
if (lang === true) {
43+
return '.js';
44+
}
45+
1846
switch (lang) {
1947
case 'ts':
2048
return '.ts';
@@ -36,7 +64,7 @@ function createTypeScriptVueExtension(
3664

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

5987
function createVueInlineScriptEmbeddedSource(
6088
text: string,
61-
lang: string | undefined
89+
lang: string | true | undefined
6290
): TypeScriptEmbeddedSource {
6391
return {
6492
sourceText: text,
@@ -71,19 +99,46 @@ function createTypeScriptVueExtension(
7199
return undefined;
72100
}
73101

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

77-
const { script } = compiler.parseComponent(vueSourceText, {
78-
pad: 'space',
79-
});
105+
let script: GenericScriptSFCBlock | undefined;
106+
if (isVueTemplateCompilerV2(compiler)) {
107+
const parsed = compiler.parseComponent(vueSourceText, {
108+
pad: 'space',
109+
});
110+
111+
script = parsed.script;
112+
} else if (isVueTemplateCompilerV3(compiler)) {
113+
const parsed = compiler.parse(vueSourceText);
114+
115+
if (parsed.descriptor && parsed.descriptor.script) {
116+
const scriptV3 = parsed.descriptor.script;
117+
118+
// map newer version of SFCScriptBlock to the generic one
119+
script = {
120+
content: scriptV3.content,
121+
attrs: scriptV3.attrs,
122+
start: scriptV3.loc.start.offset,
123+
end: scriptV3.loc.end.offset,
124+
lang: scriptV3.lang,
125+
src: scriptV3.src,
126+
};
127+
}
128+
} else {
129+
throw new Error(
130+
'Unsupported vue template compiler. Compiler should provide `parse` or `parseComponent` function.'
131+
);
132+
}
80133

81134
if (!script) {
82135
// No <script> block
83136
return createVueNoScriptEmbeddedSource();
84137
} else if (script.attrs.src) {
85138
// <script src="file.ts" /> block
86-
return createVueSrcScriptEmbeddedSource(script.attrs.src, script.attrs.lang);
139+
if (typeof script.attrs.src === 'string') {
140+
return createVueSrcScriptEmbeddedSource(script.attrs.src, script.attrs.lang);
141+
}
87142
} else {
88143
// <script lang="ts"></script> block
89144
// pad blank lines to retain diagnostics location

src/typescript-reporter/extension/vue/types/vue-template-compiler.d.ts renamed to src/typescript-reporter/extension/vue/types/vue-template-compiler.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
* This declaration is copied from https://github.com/vuejs/vue/pull/7918
33
* which may included vue-template-compiler v2.6.0.
44
*/
5-
interface SFCParserOptions {
5+
interface SFCParserOptionsV2 {
66
pad?: true | 'line' | 'space';
77
}
88

9-
export interface SFCBlock {
9+
export interface SFCBlockV2 {
1010
type: string;
1111
content: string;
1212
attrs: Record<string, string>;
@@ -18,13 +18,13 @@ export interface SFCBlock {
1818
module?: string | boolean;
1919
}
2020

21-
export interface SFCDescriptor {
22-
template: SFCBlock | undefined;
23-
script: SFCBlock | undefined;
24-
styles: SFCBlock[];
25-
customBlocks: SFCBlock[];
21+
export interface SFCDescriptorV2 {
22+
template: SFCBlockV2 | undefined;
23+
script: SFCBlockV2 | undefined;
24+
styles: SFCBlockV2[];
25+
customBlocks: SFCBlockV2[];
2626
}
2727

28-
export interface VueTemplateCompiler {
29-
parseComponent(file: string, options?: SFCParserOptions): SFCDescriptor;
28+
export interface VueTemplateCompilerV2 {
29+
parseComponent(file: string, options?: SFCParserOptionsV2): SFCDescriptorV2;
3030
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
interface Position {
2+
offset: number;
3+
line: number;
4+
column: number;
5+
}
6+
7+
interface SourceLocation {
8+
start: Position;
9+
end: Position;
10+
source: string;
11+
}
12+
13+
export interface SFCBlock {
14+
type: string;
15+
content: string;
16+
attrs: Record<string, string | true>;
17+
loc: SourceLocation;
18+
lang?: string;
19+
src?: string;
20+
}
21+
22+
interface SFCDescriptor {
23+
filename: string;
24+
template: SFCBlock | null;
25+
script: SFCBlock | null;
26+
styles: SFCBlock[];
27+
customBlocks: SFCBlock[];
28+
}
29+
30+
interface CompilerError extends SyntaxError {
31+
code: number;
32+
loc?: SourceLocation;
33+
}
34+
35+
interface SFCParseResult {
36+
descriptor: SFCDescriptor;
37+
errors: CompilerError[];
38+
}
39+
40+
export interface VueTemplateCompilerV3 {
41+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
42+
parse(template: string, options?: any): SFCParseResult;
43+
}

test/e2e/TypeScriptVueExtension.spec.ts

+75-21
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,26 @@ describe('TypeScript Vue Extension', () => {
2222
await sandbox.cleanup();
2323
});
2424

25-
it.each([{ async: false, typescript: '^3.8.0', tsloader: '^7.0.0' }])(
25+
it.each([
26+
{
27+
async: false,
28+
typescript: '^3.8.0',
29+
tsloader: '^7.0.0',
30+
vueloader: '^15.8.3',
31+
vue: '^2.6.11',
32+
compiler: 'vue-template-compiler',
33+
},
34+
{
35+
async: true,
36+
typescript: '^3.8.0',
37+
tsloader: '^7.0.0',
38+
vueloader: 'v16.0.0-beta.3',
39+
vue: '^3.0.0-beta.14',
40+
compiler: '@vue/compiler-sfc',
41+
},
42+
])(
2643
'reports semantic error for %p',
27-
async ({ async, typescript, tsloader }) => {
44+
async ({ async, typescript, tsloader, vueloader, vue, compiler }) => {
2845
await sandbox.load([
2946
await readFixture(join(__dirname, 'fixtures/environment/typescript-vue.fixture'), {
3047
FORK_TS_CHECKER_WEBPACK_PLUGIN_VERSION: JSON.stringify(
@@ -35,11 +52,48 @@ describe('TypeScript Vue Extension', () => {
3552
WEBPACK_VERSION: JSON.stringify('^4.0.0'),
3653
WEBPACK_CLI_VERSION: JSON.stringify(WEBPACK_CLI_VERSION),
3754
WEBPACK_DEV_SERVER_VERSION: JSON.stringify(WEBPACK_DEV_SERVER_VERSION),
55+
VUE_LOADER_VERSION: JSON.stringify(vueloader),
56+
VUE_VERSION: JSON.stringify(vue),
57+
VUE_COMPILER: JSON.stringify(compiler),
3858
ASYNC: JSON.stringify(async),
3959
}),
4060
await readFixture(join(__dirname, 'fixtures/implementation/typescript-vue.fixture')),
4161
]);
4262

63+
if (vue === '^2.6.11') {
64+
await sandbox.write(
65+
'src/index.ts',
66+
[
67+
"import Vue from 'vue'",
68+
"import App from './App.vue'",
69+
'',
70+
'new Vue({',
71+
' render: h => h(App)',
72+
"}).$mount('#app')",
73+
].join('\n')
74+
);
75+
await sandbox.write(
76+
'src/vue-shim.d.ts',
77+
[
78+
'declare module "*.vue" {',
79+
' import Vue from "vue";',
80+
' export default Vue;',
81+
'}',
82+
].join('\n')
83+
);
84+
} else {
85+
await sandbox.write(
86+
'src/index.ts',
87+
[
88+
"import { createApp } from 'vue'",
89+
"import App from './App.vue'",
90+
'',
91+
"createApp(App).mount('#app')",
92+
].join('\n')
93+
);
94+
await sandbox.write('src/vue-shim.d.ts', 'declare module "*.vue";');
95+
}
96+
4397
const driver = createWebpackDevServerDriver(
4498
sandbox.spawn('npm run webpack-dev-server'),
4599
async
@@ -49,7 +103,7 @@ describe('TypeScript Vue Extension', () => {
49103
// first compilation is successful
50104
await driver.waitForNoErrors();
51105

52-
// let's modify user model file
106+
// modify user model file
53107
await sandbox.patch(
54108
'src/component/LoggedIn.vue',
55109
"import User, { getUserName } from '@/model/User';",
@@ -60,43 +114,43 @@ describe('TypeScript Vue Extension', () => {
60114
errors = await driver.waitForErrors();
61115
expect(errors).toEqual([
62116
[
63-
'ERROR in src/component/LoggedIn.vue 28:24-35',
117+
'ERROR in src/component/LoggedIn.vue 27:21-32',
64118
"TS2304: Cannot find name 'getUserName'.",
119+
' 25 | const user: User = this.user;',
65120
' 26 | ',
66-
' 27 | get userName() {',
67-
" > 28 | return this.user ? getUserName(this.user) : '';",
68-
' | ^^^^^^^^^^^',
69-
' 29 | }',
70-
' 30 | ',
71-
' 31 | async logout() {',
121+
" > 27 | return user ? getUserName(user) : '';",
122+
' | ^^^^^^^^^^^',
123+
' 28 | }',
124+
' 29 | },',
125+
' 30 | async logout() {',
72126
].join('\n'),
73127
]);
74128

75-
// let's fix it
129+
// fix it
76130
await sandbox.patch(
77131
'src/component/LoggedIn.vue',
78-
"return this.user ? getUserName(this.user) : '';",
79-
"return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';"
132+
"return user ? getUserName(user) : '';",
133+
"return user ? `${user.firstName} ${user.lastName}` : '';"
80134
);
81135

82136
await driver.waitForNoErrors();
83137

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

87141
// not we should have an error about missing firstName property
88142
errors = await driver.waitForErrors();
89143
expect(errors).toEqual([
90144
[
91-
'ERROR in src/component/LoggedIn.vue 28:37-46',
145+
'ERROR in src/component/LoggedIn.vue 27:29-38',
92146
"TS2339: Property 'firstName' does not exist on type 'User'.",
147+
' 25 | const user: User = this.user;',
93148
' 26 | ',
94-
' 27 | get userName() {',
95-
" > 28 | return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';",
96-
' | ^^^^^^^^^',
97-
' 29 | }',
98-
' 30 | ',
99-
' 31 | async logout() {',
149+
" > 27 | return user ? `${user.firstName} ${user.lastName}` : '';",
150+
' | ^^^^^^^^^',
151+
' 28 | }',
152+
' 29 | },',
153+
' 30 | async logout() {',
100154
].join('\n'),
101155
[
102156
'ERROR in src/model/User.ts 11:16-25',

0 commit comments

Comments
 (0)