Skip to content

Commit

Permalink
feat: support ts in template expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Sep 20, 2021
1 parent bf42de0 commit 573fbd2
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 166 deletions.
5 changes: 4 additions & 1 deletion example/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
<span>{{ count }}</span>
<Button foo="hello!"><span>slot</span></Button>
<ScriptSetup/>
<TypeScript/>
</div>
</template>

<script>
import Button from './Button.vue'
import ScriptSetup from './ScriptSetup.vue'
import TypeScript from './TypeScript.vue'
export default {
data() {
Expand All @@ -21,7 +23,8 @@ export default {
},
components: {
Button,
ScriptSetup
ScriptSetup,
TypeScript
}
}
</script>
Expand Down
20 changes: 10 additions & 10 deletions example/ScriptSetup.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
<template>
<h2>{{ hello }}</h2>
<div>
{{ count }} <button @click="inc">+</button>
<button @click="changeColor">change color</button>
<Button />
</div>
</template>

<script setup>
import { ref } from 'vue'
import Button from './Button.vue'
ref: count = 100
let count = $ref(100)
function inc() {
count++
Expand All @@ -25,6 +16,15 @@ const changeColor = () => {
}
</script>

<template>
<h2>{{ hello }}</h2>
<div>
ref sugar count: {{ count }} <button @click="inc">+</button>
<button @click="changeColor">change color</button>
<Button />
</div>
</template>

<style>
h2 {
color: v-bind(color);
Expand Down
7 changes: 7 additions & 0 deletions example/TypeScript.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup lang="ts">
let a: number = 12
</script>

<template>
<p>From TS: {{ a?.toFixed(2) }}</p>
</template>
6 changes: 6 additions & 0 deletions example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"allowJs": true,
"sourceMap": true
}
}
63 changes: 37 additions & 26 deletions example/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,38 +80,49 @@ module.exports = (env = {}) => {
'css-loader',
],
},
// {
// test: /\.js$/,
// use: [
// {
// loader: 'cache-loader',
// options: {
// cacheIdentifier: hash(
// // deps
// fs.readFileSync(
// path.resolve(__dirname, '../package.json')
// ) +
// // env
// JSON.stringify(env) +
// // client vs. server build
// isServerBuild
// ),
// cacheDirectory: path.resolve(__dirname, '../.cache'),
// },
// },
// ...(useBabel
// ? [
// {
// loader: 'babel-loader',
// options: {
// // use yarn build-example --env.noMinimize to verify that
// // babel is properly applied to all js code, including the
// // render function compiled from SFC templates.
// presets: ['@babel/preset-env'],
// },
// },
// ]
// : []),
// ],
// },
{
test: /\.js$/,
test: /\.ts$/,
use: [
{
loader: 'cache-loader',
loader: 'ts-loader',
options: {
cacheIdentifier: hash(
// deps
fs.readFileSync(
path.resolve(__dirname, '../package.json')
) +
// env
JSON.stringify(env) +
// client vs. server build
isServerBuild
),
cacheDirectory: path.resolve(__dirname, '../.cache'),
appendTsSuffixTo: [/\.vue$/],
},
},
...(useBabel
? [
{
loader: 'babel-loader',
options: {
// use yarn build-example --env.noMinimize to verify that
// babel is properly applied to all js code, including the
// render function compiled from SFC templates.
presets: ['@babel/preset-env'],
},
},
]
: []),
],
},
// target <docs> custom blocks
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"loader-utils": "^2.0.0"
},
"peerDependencies": {
"@vue/compiler-sfc": "^3.0.8",
"@vue/compiler-sfc": "^3.2.12",
"webpack": "^4.1.0 || ^5.0.0-0"
},
"peerDependenciesMeta": {
Expand All @@ -51,12 +51,12 @@
"@types/estree": "^0.0.45",
"@types/hash-sum": "^1.0.0",
"@types/jest": "^26.0.13",
"@types/jsdom": "^16.2.4",
"@types/jsdom": "^16.2.13",
"@types/loader-utils": "^2.0.1",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/webpack": "^4.41.0",
"@types/webpack-merge": "^4.1.5",
"@vue/compiler-sfc": "^3.0.8",
"@vue/compiler-sfc": "^3.2.12",
"babel-loader": "^8.1.0",
"cache-loader": "^4.1.0",
"conventional-changelog-cli": "^2.1.1",
Expand Down Expand Up @@ -84,9 +84,9 @@
"ts-jest": "^26.2.0",
"ts-loader": "^8.0.6",
"ts-loader-v9": "npm:ts-loader@^9.2.4",
"typescript": "^4.0.2",
"typescript": "^4.4.3",
"url-loader": "^4.1.0",
"vue": "^3.0.8",
"vue": "^3.2.12",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
Expand Down
18 changes: 11 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export default function loader(
) {
const loaderContext = this

if (!/\.vue(\.html)?$/.test(loaderContext.resourcePath)) {
// ts-loader does some really weird stuff which causes vue-loader to
// somehow be applied on non-vue files... ignore them
return source
}

// check if plugin is installed
if (
!errorEmitted &&
Expand Down Expand Up @@ -150,8 +156,11 @@ export default function loader(

// script
let scriptImport = `const script = {}`
let isTS = false
const { script, scriptSetup } = descriptor
if (script || scriptSetup) {
const lang = script?.lang || scriptSetup?.lang
isTS = !!(lang && /tsx?/.test(lang))
const src = (script && !scriptSetup && script.src) || resourcePath
const attrsQuery = attrsToQuery((scriptSetup || script)!.attrs, 'js')
const query = `?vue&type=script${attrsQuery}${resourceQuery}`
Expand All @@ -172,13 +181,8 @@ export default function loader(
const idQuery = `&id=${id}`
const scopedQuery = hasScoped ? `&scoped=true` : ``
const attrsQuery = attrsToQuery(descriptor.template.attrs)
// const bindingsQuery = script
// ? `&bindings=${JSON.stringify(script.bindings ?? {})}`
// : ``
// const varsQuery = descriptor.cssVars
// ? `&vars=${qs.escape(generateCssVars(descriptor, id, isProduction))}`
// : ``
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}`
const tsQuery = isTS ? `&ts=true` : ``
const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}`
templateRequest = stringifyRequest(src + query)
templateImport = `import { ${renderFnName} } from ${templateRequest}`
}
Expand Down
7 changes: 4 additions & 3 deletions src/pluginWebpack4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,14 @@ class VueLoaderPlugin implements webpack.Plugin {
options: vueLoaderOptions,
}

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

// pitcher for block requests (for injecting stylePostLoader and deduping
Expand Down Expand Up @@ -175,7 +176,7 @@ function cloneRuleForRenderFn(rule: webpack.RuleSetRule) {
if (parsed.vue == null || parsed.type !== 'template') {
return false
}
const fakeResourcePath = `${currentResource}.js`
const fakeResourcePath = `${currentResource}.${parsed.ts ? `ts` : `js`}`
if (resource && !resource(fakeResourcePath)) {
return false
}
Expand Down
8 changes: 6 additions & 2 deletions src/pluginWebpack5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,11 @@ class VueLoaderPlugin implements Plugin {
// compiled vue render functions receive the same treatment as user code
// (mostly babel)
const jsRulesForRenderFn = rules
.filter((r) => r !== rawVueRule && match(r, 'test.js').length > 0)
.filter(
(r) =>
r !== rawVueRule &&
(match(r, 'test.js').length > 0 || match(r, 'test.ts').length > 0)
)
.map((rawRule) => cloneRule(rawRule, refs, jsRuleCheck, jsRuleResource))

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

const jsRuleResource = (query: qs.ParsedUrlQuery, resource: string): string =>
`${resource}.js`
`${resource}.${query.ts ? `ts` : `js`}`

let uid = 0

Expand Down
6 changes: 5 additions & 1 deletion src/resolveScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TemplateCompiler,
} from '@vue/compiler-sfc'
import { VueLoaderOptions } from 'src'
import { resolveTemplateTSOptions } from './util'

const clientCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
const serverCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
Expand Down Expand Up @@ -63,7 +64,10 @@ export function resolveScript(
templateOptions: {
ssr: isServer,
compiler,
compilerOptions: options.compilerOptions,
compilerOptions: {
...options.compilerOptions,
...resolveTemplateTSOptions(descriptor, options.compilerOptions),
},
transformAssetUrls: options.transformAssetUrls || true,
},
})
Expand Down
8 changes: 8 additions & 0 deletions src/templateLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { formatError } from './formatError'
import { compileTemplate, TemplateCompiler } from '@vue/compiler-sfc'
import { getDescriptor } from './descriptorCache'
import { resolveScript } from './resolveScript'
import { resolveTemplateTSOptions } from './util'

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

if (/\.[jt]sx?$/.test(loaderContext.resourcePath)) {
// ts-loader does some really weird stuff which causes vue-loader to
// somehow be applied on non-vue files... ignore them
return source
}

// although this is not the main vue-loader, we can get access to the same
// vue-loader options because we've set an ident in the plugin and used that
// ident to create the request for this loader in the pitcher.
Expand Down Expand Up @@ -55,6 +62,7 @@ const TemplateLoader: webpack.loader.Loader = function (source, inMap) {
...options.compilerOptions,
scopeId: query.scoped ? `data-v-${scopeId}` : undefined,
bindingMetadata: script ? script.bindings : undefined,
...resolveTemplateTSOptions(descriptor, options.compilerOptions),
},
transformAssetUrls: options.transformAssetUrls || true,
})
Expand Down
17 changes: 17 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SFCDescriptor, CompilerOptions } from '@vue/compiler-sfc'

export function resolveTemplateTSOptions(
descriptor: SFCDescriptor,
options: CompilerOptions | null | undefined
): CompilerOptions {
const lang = descriptor.script?.lang || descriptor.scriptSetup?.lang
const isTS = !!(lang && /tsx?$/.test(lang))
let expressionPlugins = (options && options.expressionPlugins) || []
if (isTS && !expressionPlugins.includes('typescript')) {
expressionPlugins = [...expressionPlugins, 'typescript']
}
return {
isTS,
expressionPlugins,
}
}
Loading

0 comments on commit 573fbd2

Please sign in to comment.