Skip to content

Commit 573fbd2

Browse files
committed
feat: support ts in template expressions
1 parent bf42de0 commit 573fbd2

13 files changed

+229
-166
lines changed

example/App.vue

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
<span>{{ count }}</span>
66
<Button foo="hello!"><span>slot</span></Button>
77
<ScriptSetup/>
8+
<TypeScript/>
89
</div>
910
</template>
1011

1112
<script>
1213
import Button from './Button.vue'
1314
import ScriptSetup from './ScriptSetup.vue'
15+
import TypeScript from './TypeScript.vue'
1416
1517
export default {
1618
data() {
@@ -21,7 +23,8 @@ export default {
2123
},
2224
components: {
2325
Button,
24-
ScriptSetup
26+
ScriptSetup,
27+
TypeScript
2528
}
2629
}
2730
</script>

example/ScriptSetup.vue

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
1-
<template>
2-
<h2>{{ hello }}</h2>
3-
<div>
4-
{{ count }} <button @click="inc">+</button>
5-
<button @click="changeColor">change color</button>
6-
<Button />
7-
</div>
8-
</template>
9-
101
<script setup>
112
import { ref } from 'vue'
123
import Button from './Button.vue'
134
14-
ref: count = 100
5+
let count = $ref(100)
156
167
function inc() {
178
count++
@@ -25,6 +16,15 @@ const changeColor = () => {
2516
}
2617
</script>
2718

19+
<template>
20+
<h2>{{ hello }}</h2>
21+
<div>
22+
ref sugar count: {{ count }} <button @click="inc">+</button>
23+
<button @click="changeColor">change color</button>
24+
<Button />
25+
</div>
26+
</template>
27+
2828
<style>
2929
h2 {
3030
color: v-bind(color);

example/TypeScript.vue

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script setup lang="ts">
2+
let a: number = 12
3+
</script>
4+
5+
<template>
6+
<p>From TS: {{ a?.toFixed(2) }}</p>
7+
</template>

example/tsconfig.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"compilerOptions": {
3+
"allowJs": true,
4+
"sourceMap": true
5+
}
6+
}

example/webpack.config.js

+37-26
Original file line numberDiff line numberDiff line change
@@ -80,38 +80,49 @@ module.exports = (env = {}) => {
8080
'css-loader',
8181
],
8282
},
83+
// {
84+
// test: /\.js$/,
85+
// use: [
86+
// {
87+
// loader: 'cache-loader',
88+
// options: {
89+
// cacheIdentifier: hash(
90+
// // deps
91+
// fs.readFileSync(
92+
// path.resolve(__dirname, '../package.json')
93+
// ) +
94+
// // env
95+
// JSON.stringify(env) +
96+
// // client vs. server build
97+
// isServerBuild
98+
// ),
99+
// cacheDirectory: path.resolve(__dirname, '../.cache'),
100+
// },
101+
// },
102+
// ...(useBabel
103+
// ? [
104+
// {
105+
// loader: 'babel-loader',
106+
// options: {
107+
// // use yarn build-example --env.noMinimize to verify that
108+
// // babel is properly applied to all js code, including the
109+
// // render function compiled from SFC templates.
110+
// presets: ['@babel/preset-env'],
111+
// },
112+
// },
113+
// ]
114+
// : []),
115+
// ],
116+
// },
83117
{
84-
test: /\.js$/,
118+
test: /\.ts$/,
85119
use: [
86120
{
87-
loader: 'cache-loader',
121+
loader: 'ts-loader',
88122
options: {
89-
cacheIdentifier: hash(
90-
// deps
91-
fs.readFileSync(
92-
path.resolve(__dirname, '../package.json')
93-
) +
94-
// env
95-
JSON.stringify(env) +
96-
// client vs. server build
97-
isServerBuild
98-
),
99-
cacheDirectory: path.resolve(__dirname, '../.cache'),
123+
appendTsSuffixTo: [/\.vue$/],
100124
},
101125
},
102-
...(useBabel
103-
? [
104-
{
105-
loader: 'babel-loader',
106-
options: {
107-
// use yarn build-example --env.noMinimize to verify that
108-
// babel is properly applied to all js code, including the
109-
// render function compiled from SFC templates.
110-
presets: ['@babel/preset-env'],
111-
},
112-
},
113-
]
114-
: []),
115126
],
116127
},
117128
// target <docs> custom blocks

package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"loader-utils": "^2.0.0"
3838
},
3939
"peerDependencies": {
40-
"@vue/compiler-sfc": "^3.0.8",
40+
"@vue/compiler-sfc": "^3.2.12",
4141
"webpack": "^4.1.0 || ^5.0.0-0"
4242
},
4343
"peerDependenciesMeta": {
@@ -51,12 +51,12 @@
5151
"@types/estree": "^0.0.45",
5252
"@types/hash-sum": "^1.0.0",
5353
"@types/jest": "^26.0.13",
54-
"@types/jsdom": "^16.2.4",
54+
"@types/jsdom": "^16.2.13",
5555
"@types/loader-utils": "^2.0.1",
5656
"@types/mini-css-extract-plugin": "^0.9.1",
5757
"@types/webpack": "^4.41.0",
5858
"@types/webpack-merge": "^4.1.5",
59-
"@vue/compiler-sfc": "^3.0.8",
59+
"@vue/compiler-sfc": "^3.2.12",
6060
"babel-loader": "^8.1.0",
6161
"cache-loader": "^4.1.0",
6262
"conventional-changelog-cli": "^2.1.1",
@@ -84,9 +84,9 @@
8484
"ts-jest": "^26.2.0",
8585
"ts-loader": "^8.0.6",
8686
"ts-loader-v9": "npm:ts-loader@^9.2.4",
87-
"typescript": "^4.0.2",
87+
"typescript": "^4.4.3",
8888
"url-loader": "^4.1.0",
89-
"vue": "^3.0.8",
89+
"vue": "^3.2.12",
9090
"webpack": "^4.41.2",
9191
"webpack-cli": "^3.3.10",
9292
"webpack-dev-server": "^3.9.0",

src/index.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export default function loader(
5757
) {
5858
const loaderContext = this
5959

60+
if (!/\.vue(\.html)?$/.test(loaderContext.resourcePath)) {
61+
// ts-loader does some really weird stuff which causes vue-loader to
62+
// somehow be applied on non-vue files... ignore them
63+
return source
64+
}
65+
6066
// check if plugin is installed
6167
if (
6268
!errorEmitted &&
@@ -150,8 +156,11 @@ export default function loader(
150156

151157
// script
152158
let scriptImport = `const script = {}`
159+
let isTS = false
153160
const { script, scriptSetup } = descriptor
154161
if (script || scriptSetup) {
162+
const lang = script?.lang || scriptSetup?.lang
163+
isTS = !!(lang && /tsx?/.test(lang))
155164
const src = (script && !scriptSetup && script.src) || resourcePath
156165
const attrsQuery = attrsToQuery((scriptSetup || script)!.attrs, 'js')
157166
const query = `?vue&type=script${attrsQuery}${resourceQuery}`
@@ -172,13 +181,8 @@ export default function loader(
172181
const idQuery = `&id=${id}`
173182
const scopedQuery = hasScoped ? `&scoped=true` : ``
174183
const attrsQuery = attrsToQuery(descriptor.template.attrs)
175-
// const bindingsQuery = script
176-
// ? `&bindings=${JSON.stringify(script.bindings ?? {})}`
177-
// : ``
178-
// const varsQuery = descriptor.cssVars
179-
// ? `&vars=${qs.escape(generateCssVars(descriptor, id, isProduction))}`
180-
// : ``
181-
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}`
184+
const tsQuery = isTS ? `&ts=true` : ``
185+
const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}`
182186
templateRequest = stringifyRequest(src + query)
183187
templateImport = `import { ${renderFnName} } from ${templateRequest}`
184188
}

src/pluginWebpack4.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,14 @@ class VueLoaderPlugin implements webpack.Plugin {
7575
options: vueLoaderOptions,
7676
}
7777

78-
// for each rule that matches plain .js files, also create a clone and
78+
// for each rule that matches plain .js/.ts files, also create a clone and
7979
// match it against the compiled template code inside *.vue files, so that
8080
// compiled vue render functions receive the same treatment as user code
8181
// (mostly babel)
8282
const matchesJS = createMatcher(`test.js`)
83+
const matchesTS = createMatcher(`test.ts`)
8384
const jsRulesForRenderFn = rules
84-
.filter((r) => r !== vueRule && matchesJS(r))
85+
.filter((r) => r !== vueRule && (matchesJS(r) || matchesTS(r)))
8586
.map(cloneRuleForRenderFn)
8687

8788
// pitcher for block requests (for injecting stylePostLoader and deduping
@@ -175,7 +176,7 @@ function cloneRuleForRenderFn(rule: webpack.RuleSetRule) {
175176
if (parsed.vue == null || parsed.type !== 'template') {
176177
return false
177178
}
178-
const fakeResourcePath = `${currentResource}.js`
179+
const fakeResourcePath = `${currentResource}.${parsed.ts ? `ts` : `js`}`
179180
if (resource && !resource(fakeResourcePath)) {
180181
return false
181182
}

src/pluginWebpack5.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,11 @@ class VueLoaderPlugin implements Plugin {
195195
// compiled vue render functions receive the same treatment as user code
196196
// (mostly babel)
197197
const jsRulesForRenderFn = rules
198-
.filter((r) => r !== rawVueRule && match(r, 'test.js').length > 0)
198+
.filter(
199+
(r) =>
200+
r !== rawVueRule &&
201+
(match(r, 'test.js').length > 0 || match(r, 'test.ts').length > 0)
202+
)
199203
.map((rawRule) => cloneRule(rawRule, refs, jsRuleCheck, jsRuleResource))
200204

201205
// global pitcher (responsible for injecting template compiler loader & CSS
@@ -259,7 +263,7 @@ const jsRuleCheck = (query: qs.ParsedUrlQuery): boolean => {
259263
}
260264

261265
const jsRuleResource = (query: qs.ParsedUrlQuery, resource: string): string =>
262-
`${resource}.js`
266+
`${resource}.${query.ts ? `ts` : `js`}`
263267

264268
let uid = 0
265269

src/resolveScript.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
TemplateCompiler,
77
} from '@vue/compiler-sfc'
88
import { VueLoaderOptions } from 'src'
9+
import { resolveTemplateTSOptions } from './util'
910

1011
const clientCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
1112
const serverCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
@@ -63,7 +64,10 @@ export function resolveScript(
6364
templateOptions: {
6465
ssr: isServer,
6566
compiler,
66-
compilerOptions: options.compilerOptions,
67+
compilerOptions: {
68+
...options.compilerOptions,
69+
...resolveTemplateTSOptions(descriptor, options.compilerOptions),
70+
},
6771
transformAssetUrls: options.transformAssetUrls || true,
6872
},
6973
})

src/templateLoader.ts

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { formatError } from './formatError'
66
import { compileTemplate, TemplateCompiler } from '@vue/compiler-sfc'
77
import { getDescriptor } from './descriptorCache'
88
import { resolveScript } from './resolveScript'
9+
import { resolveTemplateTSOptions } from './util'
910

1011
// Loader that compiles raw template into JavaScript functions.
1112
// This is injected by the global pitcher (../pitch) for template
@@ -14,6 +15,12 @@ const TemplateLoader: webpack.loader.Loader = function (source, inMap) {
1415
source = String(source)
1516
const loaderContext = this
1617

18+
if (/\.[jt]sx?$/.test(loaderContext.resourcePath)) {
19+
// ts-loader does some really weird stuff which causes vue-loader to
20+
// somehow be applied on non-vue files... ignore them
21+
return source
22+
}
23+
1724
// although this is not the main vue-loader, we can get access to the same
1825
// vue-loader options because we've set an ident in the plugin and used that
1926
// ident to create the request for this loader in the pitcher.
@@ -55,6 +62,7 @@ const TemplateLoader: webpack.loader.Loader = function (source, inMap) {
5562
...options.compilerOptions,
5663
scopeId: query.scoped ? `data-v-${scopeId}` : undefined,
5764
bindingMetadata: script ? script.bindings : undefined,
65+
...resolveTemplateTSOptions(descriptor, options.compilerOptions),
5866
},
5967
transformAssetUrls: options.transformAssetUrls || true,
6068
})

src/util.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { SFCDescriptor, CompilerOptions } from '@vue/compiler-sfc'
2+
3+
export function resolveTemplateTSOptions(
4+
descriptor: SFCDescriptor,
5+
options: CompilerOptions | null | undefined
6+
): CompilerOptions {
7+
const lang = descriptor.script?.lang || descriptor.scriptSetup?.lang
8+
const isTS = !!(lang && /tsx?$/.test(lang))
9+
let expressionPlugins = (options && options.expressionPlugins) || []
10+
if (isTS && !expressionPlugins.includes('typescript')) {
11+
expressionPlugins = [...expressionPlugins, 'typescript']
12+
}
13+
return {
14+
isTS,
15+
expressionPlugins,
16+
}
17+
}

0 commit comments

Comments
 (0)