Skip to content

Commit bb0c105

Browse files
committedDec 17, 2020
wip: port rollup-plugin-vue to vite plugin
1 parent 9eef197 commit bb0c105

13 files changed

+1027
-3
lines changed
 

‎LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019-present, Yuxi (Evan) You
3+
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"packages/*"
66
],
77
"scripts": {
8-
"lint": "eslint --ext .ts packages/*/src/**"
8+
"lint": "eslint --ext .ts packages/*/src/**",
9+
"bundle": "esbuild packages/vite/src/node/index.ts packages/vite/src/node/cli.ts --bundle --platform=node --target=node12 --external:fsevents --external:sugarss --external:bufferutil --external:utf-8-validate --outdir=esbuild"
910
},
1011
"devDependencies": {
1112
"@types/node": "^14.14.10",

‎packages/plugin-vue/package.json

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,36 @@
11
{
22
"name": "@vitejs/plugin-vue",
3-
"version": "1.0.0"
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"files": [
6+
"dist"
7+
],
8+
"main": "dist/index.js",
9+
"types": "dist/index.d.ts",
10+
"scripts": {
11+
"dev": "tsc -w --incremental -p ."
12+
},
13+
"engines": {
14+
"node": ">=12.0.0"
15+
},
16+
"repository": {
17+
"type": "git",
18+
"url": "git+https://github.com/vitejs/vite.git"
19+
},
20+
"bugs": {
21+
"url": "https://github.com/vitejs/vite/issues"
22+
},
23+
"homepage": "https://github.com/vitejs/vite/tree/master/#readme",
24+
"peerDependencies": {
25+
"@vue/compiler-sfc": "^3.0.4"
26+
},
27+
"devDependencies": {
28+
"@rollup/pluginutils": "^4.1.0",
29+
"@types/hash-sum": "^1.0.0",
30+
"@vue/compiler-sfc": "^3.0.4",
31+
"debug": "^4.3.1",
32+
"hash-sum": "^2.0.0",
33+
"rollup": "^2.35.1",
34+
"source-map": "^0.6.1"
35+
}
436
}
+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import fs from 'fs'
2+
import _debug from 'debug'
3+
import { parse, SFCBlock, SFCDescriptor } from '@vue/compiler-sfc'
4+
import {
5+
getDescriptor,
6+
setDescriptor,
7+
setPrevDescriptor
8+
} from './utils/descriptorCache'
9+
import { getResolvedScript, setResolvedScript } from './script'
10+
import { ModuleNode } from 'vite'
11+
12+
const debug = _debug('vite:hmr')
13+
14+
/**
15+
* Vite-specific HMR handling
16+
*/
17+
export async function handleHotUpdate(
18+
file: string,
19+
modules: ModuleNode[]
20+
): Promise<ModuleNode[] | void> {
21+
if (!file.endsWith('.vue')) {
22+
return
23+
}
24+
25+
const prevDescriptor = getDescriptor(file)
26+
if (!prevDescriptor) {
27+
// file hasn't been requested yet (e.g. async component)
28+
return
29+
}
30+
31+
let content = fs.readFileSync(file, 'utf-8')
32+
if (!content) {
33+
await untilModified(file)
34+
content = fs.readFileSync(file, 'utf-8')
35+
}
36+
37+
const { descriptor } = parse(content, {
38+
filename: file,
39+
sourceMap: true
40+
})
41+
setDescriptor(file, descriptor)
42+
setPrevDescriptor(file, prevDescriptor)
43+
44+
let needRerender = false
45+
const affectedModules = new Set<ModuleNode | undefined>()
46+
const mainModule = modules.find(
47+
(m) => !/type=/.test(m.url) || /type=script/.test(m.url)
48+
)
49+
const templateModule = modules.find((m) => /type=template/.test(m.url))
50+
51+
if (
52+
!isEqualBlock(descriptor.script, prevDescriptor.script) ||
53+
!isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup)
54+
) {
55+
affectedModules.add(mainModule)
56+
}
57+
58+
if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
59+
// when a <script setup> component's template changes, it will need correct
60+
// binding metadata. However, when reloading the template alone the binding
61+
// metadata will not be available since the script part isn't loaded.
62+
// in this case, reuse the compiled script from previous descriptor.
63+
if (mainModule && !affectedModules.has(mainModule)) {
64+
setResolvedScript(descriptor, getResolvedScript(prevDescriptor)!)
65+
}
66+
affectedModules.add(templateModule)
67+
needRerender = true
68+
}
69+
70+
let didUpdateStyle = false
71+
const prevStyles = prevDescriptor.styles || []
72+
const nextStyles = descriptor.styles || []
73+
74+
// force reload if CSS vars injection changed
75+
if (prevDescriptor.cssVars.join('') !== descriptor.cssVars.join('')) {
76+
affectedModules.add(mainModule)
77+
}
78+
79+
// force reload if scoped status has changed
80+
if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) {
81+
// template needs to be invalidated as well
82+
affectedModules.add(templateModule)
83+
affectedModules.add(mainModule)
84+
}
85+
86+
// only need to update styles if not reloading, since reload forces
87+
// style updates as well.
88+
for (let i = 0; i < nextStyles.length; i++) {
89+
const prev = prevStyles[i]
90+
const next = nextStyles[i]
91+
if (!prev || !isEqualBlock(prev, next)) {
92+
didUpdateStyle = true
93+
const mod = modules.find((m) => m.url.includes(`type=style&index=${i}`))
94+
if (mod) {
95+
affectedModules.add(mod)
96+
} else {
97+
// new style block - force reload
98+
affectedModules.add(mainModule)
99+
}
100+
}
101+
}
102+
if (prevStyles.length > nextStyles.length) {
103+
// style block removed - force reload
104+
affectedModules.add(mainModule)
105+
}
106+
107+
const prevCustoms = prevDescriptor.customBlocks || []
108+
const nextCustoms = descriptor.customBlocks || []
109+
110+
// custom blocks update causes a reload
111+
// because the custom block contents is changed and it may be used in JS.
112+
for (let i = 0; i < nextCustoms.length; i++) {
113+
const prev = prevCustoms[i]
114+
const next = nextCustoms[i]
115+
if (!prev || !isEqualBlock(prev, next)) {
116+
const mod = modules.find((m) =>
117+
m.url.includes(`type=${prev.type}&index=${i}`)
118+
)
119+
if (mod) {
120+
affectedModules.add(mod)
121+
} else {
122+
affectedModules.add(mainModule)
123+
}
124+
}
125+
}
126+
if (prevCustoms.length > nextCustoms.length) {
127+
// block rmeoved, force reload
128+
affectedModules.add(mainModule)
129+
}
130+
131+
let updateType = []
132+
if (needRerender) {
133+
updateType.push(`template`)
134+
// template is inlined into main, add main module instead
135+
if (!templateModule) {
136+
affectedModules.add(mainModule)
137+
}
138+
}
139+
if (didUpdateStyle) {
140+
updateType.push(`style`)
141+
}
142+
if (updateType.length) {
143+
debug(`[vue:update(${updateType.join('&')})] ${file}`)
144+
}
145+
return [...affectedModules].filter(Boolean) as ModuleNode[]
146+
}
147+
148+
// vitejs/vite#610 when hot-reloading Vue files, we read immediately on file
149+
// change event and sometimes this can be too early and get an empty buffer.
150+
// Poll until the file's modified time has changed before reading again.
151+
async function untilModified(file: string) {
152+
const mtime = fs.statSync(file).mtimeMs
153+
return new Promise((r) => {
154+
let n = 0
155+
const poll = async () => {
156+
n++
157+
const newMtime = fs.statSync(file).mtimeMs
158+
if (newMtime !== mtime || n > 10) {
159+
r(0)
160+
} else {
161+
setTimeout(poll, 10)
162+
}
163+
}
164+
setTimeout(poll, 10)
165+
})
166+
}
167+
168+
function isEqualBlock(a: SFCBlock | null, b: SFCBlock | null) {
169+
if (!a && !b) return true
170+
if (!a || !b) return false
171+
// src imports will trigger their own updates
172+
if (a.src && b.src && a.src === b.src) return true
173+
if (a.content !== b.content) return false
174+
const keysA = Object.keys(a.attrs)
175+
const keysB = Object.keys(b.attrs)
176+
if (keysA.length !== keysB.length) {
177+
return false
178+
}
179+
return keysA.every((key) => a.attrs[key] === b.attrs[key])
180+
}
181+
182+
export function isOnlyTemplateChanged(
183+
prev: SFCDescriptor,
184+
next: SFCDescriptor
185+
) {
186+
return (
187+
isEqualBlock(prev.script, next.script) &&
188+
isEqualBlock(prev.scriptSetup, next.scriptSetup) &&
189+
prev.styles.length === next.styles.length &&
190+
prev.styles.every((s, i) => isEqualBlock(s, next.styles[i])) &&
191+
prev.customBlocks.length === next.customBlocks.length &&
192+
prev.customBlocks.every((s, i) => isEqualBlock(s, next.customBlocks[i]))
193+
)
194+
}

‎packages/plugin-vue/src/index.ts

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
try {
2+
require.resolve('@vue/compiler-sfc')
3+
} catch (e) {
4+
throw new Error(
5+
'@vitejs/plugin-vue requires @vue/compiler-sfc to be present in the dependency ' +
6+
'tree.'
7+
)
8+
}
9+
10+
import fs from 'fs'
11+
import { Plugin, ViteDevServer } from 'vite'
12+
import { createFilter } from '@rollup/pluginutils'
13+
import {
14+
SFCBlock,
15+
SFCScriptCompileOptions,
16+
SFCStyleCompileOptions,
17+
SFCTemplateCompileOptions
18+
} from '@vue/compiler-sfc'
19+
import { parseVueRequest } from './utils/query'
20+
import { getDescriptor, setDescriptor } from './utils/descriptorCache'
21+
import { getResolvedScript } from './script'
22+
import { transformMain } from './main'
23+
import { handleHotUpdate } from './handleHotUpdate'
24+
import { transformTemplateAsModule } from './template'
25+
import { transformStyle } from './style'
26+
27+
// extend the descriptor so we can store the scopeId on it
28+
declare module '@vue/compiler-sfc' {
29+
interface SFCDescriptor {
30+
id: string
31+
}
32+
}
33+
34+
export interface Options {
35+
include?: string | RegExp | (string | RegExp)[]
36+
exclude?: string | RegExp | (string | RegExp)[]
37+
38+
ssr?: boolean
39+
isProduction?: boolean
40+
41+
// options to pass on to @vue/compiler-sfc
42+
script?: SFCScriptCompileOptions
43+
template?: SFCTemplateCompileOptions
44+
style?: SFCStyleCompileOptions
45+
}
46+
47+
export interface ResolvedOptions extends Options {
48+
root: string
49+
devServer?: ViteDevServer
50+
}
51+
52+
export default function vuePlugin(rawOptions: Options = {}): Plugin {
53+
let options: ResolvedOptions = {
54+
ssr: false,
55+
isProduction: process.env.NODE_ENV === 'production',
56+
...rawOptions,
57+
root: process.cwd()
58+
}
59+
60+
const filter = createFilter(
61+
rawOptions.include || /\.vue$/,
62+
rawOptions.exclude
63+
)
64+
65+
return {
66+
name: 'vite:vue',
67+
68+
handleHotUpdate,
69+
70+
configResolved(config) {
71+
options = {
72+
...options,
73+
root: config.root,
74+
isProduction: config.isProduction,
75+
ssr: !!config.build.ssr
76+
}
77+
},
78+
79+
configureServer(server) {
80+
options.devServer = server
81+
},
82+
83+
async resolveId(id, importer) {
84+
const { filename, query } = parseVueRequest(id)
85+
// serve subpart requests (*?vue) as virtual modules
86+
if (query.vue) {
87+
if (query.src) {
88+
const resolved = await this.resolve(filename, importer, {
89+
skipSelf: true
90+
})
91+
if (resolved) {
92+
// associate this imported file to the importer SFC's descriptor
93+
// so that we can retrieve it in transform()
94+
setDescriptor(resolved.id, getDescriptor(importer!))
95+
const [, originalQuery] = id.split('?', 2)
96+
resolved.id += `?${originalQuery}`
97+
return resolved
98+
}
99+
} else if (!filter(filename)) {
100+
return null
101+
}
102+
return id
103+
}
104+
},
105+
106+
load(id) {
107+
const { filename, query } = parseVueRequest(id)
108+
if (!filter(filename)) {
109+
return
110+
}
111+
// serve subpart virtual modules
112+
if (query.vue) {
113+
if (query.src) {
114+
return fs.readFileSync(filename, 'utf-8')
115+
}
116+
const descriptor = getDescriptor(filename)
117+
let block: SFCBlock | null | undefined
118+
if (query.type === 'script') {
119+
// handle <scrip> + <script setup> merge via compileScript()
120+
block = getResolvedScript(descriptor, options.ssr)
121+
} else if (query.type === 'template') {
122+
block = descriptor.template!
123+
} else if (query.type === 'style') {
124+
block = descriptor.styles[query.index!]
125+
} else if (query.index) {
126+
block = descriptor.customBlocks[query.index]
127+
}
128+
if (block) {
129+
return {
130+
code: block.content,
131+
map: block.map as any
132+
}
133+
}
134+
}
135+
},
136+
137+
transform(code, id) {
138+
const { filename, query } = parseVueRequest(id)
139+
if (!filter(filename)) {
140+
return
141+
}
142+
143+
if (!query.vue) {
144+
// main request
145+
return transformMain(code, filename, options, this)
146+
}
147+
148+
if (query.vue) {
149+
const descriptor = getDescriptor(filename)
150+
if (query.type === 'template') {
151+
return transformTemplateAsModule(code, descriptor, options, this)
152+
} else if (query.type === 'style') {
153+
return transformStyle(
154+
code,
155+
descriptor,
156+
Number(query.index),
157+
options,
158+
this
159+
)
160+
}
161+
}
162+
}
163+
}
164+
}
165+
166+
// overwrite for cjs require('...')() usage
167+
module.exports = vuePlugin

‎packages/plugin-vue/src/main.ts

+328
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
import hash from 'hash-sum'
2+
import path from 'path'
3+
import qs from 'querystring'
4+
import {
5+
parse,
6+
rewriteDefault,
7+
SFCBlock,
8+
SFCDescriptor
9+
} from '@vue/compiler-sfc'
10+
import { ResolvedOptions } from '.'
11+
import { getPrevDescriptor, setDescriptor } from './utils/descriptorCache'
12+
import { PluginContext, TransformPluginContext } from 'rollup'
13+
import { resolveScript } from './script'
14+
import { transformTemplateInMain } from './template'
15+
import { isOnlyTemplateChanged } from './handleHotUpdate'
16+
import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map'
17+
import { createRollupError } from './utils/error'
18+
19+
export async function transformMain(
20+
code: string,
21+
filename: string,
22+
options: ResolvedOptions,
23+
pluginContext: TransformPluginContext
24+
) {
25+
const { root, devServer, isProduction, ssr } = options
26+
27+
// prev descriptor is only set and used for hmr
28+
const prevDescriptor = getPrevDescriptor(filename)
29+
const { descriptor, errors } = parse(code, {
30+
sourceMap: true,
31+
filename
32+
})
33+
34+
// set the id on the descriptor
35+
const shortFilePath = path
36+
.relative(root, filename)
37+
.replace(/^(\.\.[\/\\])+/, '')
38+
.replace(/\\/g, '/')
39+
40+
descriptor.id = hash(
41+
isProduction ? shortFilePath + '\n' + code : shortFilePath
42+
)
43+
44+
setDescriptor(filename, descriptor)
45+
46+
if (errors.length) {
47+
errors.forEach((error) =>
48+
pluginContext.error(createRollupError(filename, error))
49+
)
50+
return null
51+
}
52+
53+
// feature information
54+
const hasScoped = descriptor.styles.some((s) => s.scoped)
55+
56+
// script
57+
const { code: scriptCode, map } = await genScriptCode(descriptor, options)
58+
59+
// template
60+
// Check if we can use compile template as inlined render function
61+
// inside <script setup>. This can only be done for build because
62+
// inlined template cannot be individually hot updated.
63+
const useInlineTemplate =
64+
!devServer &&
65+
descriptor.scriptSetup &&
66+
!(descriptor.template && descriptor.template.src)
67+
const hasTemplateImport = descriptor.template && !useInlineTemplate
68+
69+
let templateCode = ''
70+
let templateMap
71+
if (hasTemplateImport) {
72+
;({ code: templateCode, map: templateMap } = genTemplateCode(
73+
descriptor,
74+
options,
75+
pluginContext
76+
))
77+
}
78+
79+
const renderReplace = hasTemplateImport
80+
? ssr
81+
? `_sfc_main.ssrRender = _sfc_ssrRender`
82+
: `_sfc_main.render = _sfc_render`
83+
: ''
84+
85+
// styles
86+
const stylesCode = genStyleCode(descriptor)
87+
88+
// custom blocks
89+
const customBlocksCode = genCustomBlockCode(descriptor)
90+
91+
const output: string[] = [
92+
scriptCode,
93+
templateCode,
94+
stylesCode,
95+
customBlocksCode,
96+
renderReplace
97+
]
98+
if (hasScoped) {
99+
output.push(
100+
`_sfc_main.__scopeId = ${JSON.stringify(`data-v-${descriptor.id}`)}`
101+
)
102+
}
103+
if (!isProduction) {
104+
output.push(`_sfc_main.__file = ${JSON.stringify(shortFilePath)}`)
105+
} else if (devServer) {
106+
// expose filename during serve for devtools to pickup
107+
output.push(
108+
`_sfc_main.__file = ${JSON.stringify(path.basename(shortFilePath))}`
109+
)
110+
}
111+
output.push('export default _sfc_main')
112+
113+
// HMR
114+
if (devServer) {
115+
// check if the template is the only thing that changed
116+
if (prevDescriptor && isOnlyTemplateChanged(prevDescriptor, descriptor)) {
117+
output.push(`export const _rerender_only = true`)
118+
}
119+
output.push(`_sfc_main.__hmrId = ${JSON.stringify(descriptor.id)}`)
120+
output.push(
121+
`__VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)`
122+
)
123+
output.push(
124+
`import.meta.hot.accept(({ default: updated, _rerender_only }) => {`,
125+
` if (_rerender_only) {`,
126+
` __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)`,
127+
` } else {`,
128+
` __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)`,
129+
` }`,
130+
`})`
131+
)
132+
}
133+
134+
// if the template is inlined into the main module (indicated by the presence
135+
// of templateMap, we need to concatenate the two source maps.
136+
let resolvedMap = map
137+
if (map && templateMap) {
138+
const generator = SourceMapGenerator.fromSourceMap(
139+
new SourceMapConsumer(map)
140+
)
141+
const offset = scriptCode.match(/\r?\n/g)?.length || 1
142+
const templateMapConsumer = new SourceMapConsumer(templateMap)
143+
templateMapConsumer.eachMapping((m) => {
144+
generator.addMapping({
145+
source: m.source,
146+
original: { line: m.originalLine, column: m.originalColumn },
147+
generated: {
148+
line: m.generatedLine + offset,
149+
column: m.generatedColumn
150+
}
151+
})
152+
})
153+
resolvedMap = (generator as any).toJSON()
154+
// if this is a template only update, we will be reusing a cached version
155+
// of the main module compile result, which has outdated sourcesContent.
156+
resolvedMap.sourcesContent = templateMap.sourcesContent
157+
}
158+
159+
return {
160+
code: output.join('\n'),
161+
map: resolvedMap || {
162+
mappings: ''
163+
}
164+
}
165+
}
166+
167+
function genTemplateCode(
168+
descriptor: SFCDescriptor,
169+
options: ResolvedOptions,
170+
pluginContext: PluginContext
171+
) {
172+
const renderFnName = options.ssr ? 'ssrRender' : 'render'
173+
const template = descriptor.template!
174+
175+
// If the template is not using pre-processor AND is not using external src,
176+
// compile and inline it directly in the main module. When served in vite this
177+
// saves an extra request per SFC which can improve load performance.
178+
if (!template.lang && !template.src) {
179+
return transformTemplateInMain(
180+
template.content,
181+
descriptor,
182+
options,
183+
pluginContext
184+
)
185+
} else {
186+
const src = template.src || descriptor.filename
187+
const srcQuery = template.src ? `&src` : ``
188+
const attrsQuery = attrsToQuery(template.attrs, 'js', true)
189+
const query = `?vue&type=template${srcQuery}${attrsQuery}`
190+
return {
191+
code: `import { ${renderFnName} as _sfc_${renderFnName} } from ${JSON.stringify(
192+
src + query
193+
)}`,
194+
map: undefined
195+
}
196+
}
197+
}
198+
199+
async function genScriptCode(
200+
descriptor: SFCDescriptor,
201+
options: ResolvedOptions
202+
): Promise<{
203+
code: string
204+
map: RawSourceMap
205+
}> {
206+
let scriptCode = `const _sfc_main = {}`
207+
let map
208+
const script = resolveScript(descriptor, options)
209+
if (script) {
210+
// If the script is js/ts and has no external src, it can be directly placed
211+
// in the main module.
212+
if (
213+
(!script.lang || (script.lang === 'ts' && options.devServer)) &&
214+
!script.src
215+
) {
216+
scriptCode = rewriteDefault(script.content, `_sfc_main`)
217+
map = script.map
218+
if (script.lang === 'ts') {
219+
const result = await options.devServer!.transformWithEsbuild(
220+
scriptCode,
221+
descriptor.filename,
222+
{ loader: 'ts' },
223+
map
224+
)
225+
scriptCode = result.code
226+
map = result.map
227+
}
228+
} else {
229+
const src = script.src || descriptor.filename
230+
const attrsQuery = attrsToQuery(script.attrs, 'js')
231+
const srcQuery = script.src ? `&src` : ``
232+
const query = `?vue&type=script${srcQuery}${attrsQuery}`
233+
const scriptRequest = JSON.stringify(src + query)
234+
scriptCode =
235+
`import _sfc_main from ${scriptRequest}\n` +
236+
`export * from ${scriptRequest}` // support named exports
237+
}
238+
}
239+
return {
240+
code: scriptCode,
241+
map: map as any
242+
}
243+
}
244+
245+
function genStyleCode(descriptor: SFCDescriptor) {
246+
let stylesCode = ``
247+
let hasCSSModules = false
248+
if (descriptor.styles.length) {
249+
descriptor.styles.forEach((style, i) => {
250+
const src = style.src || descriptor.filename
251+
// do not include module in default query, since we use it to indicate
252+
// that the module needs to export the modules json
253+
const attrsQuery = attrsToQuery(style.attrs, 'css')
254+
const srcQuery = style.src ? `&src` : ``
255+
const query = `?vue&type=style&index=${i}${srcQuery}`
256+
const styleRequest = src + query + attrsQuery
257+
if (style.module) {
258+
if (!hasCSSModules) {
259+
stylesCode += `\nconst cssModules = _sfc_main.__cssModules = {}`
260+
hasCSSModules = true
261+
}
262+
stylesCode += genCSSModulesCode(i, styleRequest, style.module)
263+
} else {
264+
stylesCode += `\nimport ${JSON.stringify(styleRequest)}`
265+
}
266+
// TODO SSR critical CSS collection
267+
})
268+
}
269+
return stylesCode
270+
}
271+
272+
function genCustomBlockCode(descriptor: SFCDescriptor) {
273+
let code = ''
274+
descriptor.customBlocks.forEach((block, index) => {
275+
const src = block.src || descriptor.filename
276+
const attrsQuery = attrsToQuery(block.attrs, block.type)
277+
const srcQuery = block.src ? `&src` : ``
278+
const query = `?vue&type=${block.type}&index=${index}${srcQuery}${attrsQuery}`
279+
const request = JSON.stringify(src + query)
280+
code += `import block${index} from ${request}\n`
281+
code += `if (typeof block${index} === 'function') block${index}(_sfc_main)\n`
282+
})
283+
return code
284+
}
285+
286+
function genCSSModulesCode(
287+
index: number,
288+
request: string,
289+
moduleName: string | boolean
290+
): string {
291+
const styleVar = `style${index}`
292+
const exposedName = typeof moduleName === 'string' ? moduleName : '$style'
293+
// inject `.module` before extension so vite handles it as css module
294+
const moduleRequest = request.replace(/\.(\w+)$/, '.module.$1')
295+
return (
296+
`\nimport ${styleVar} from ${JSON.stringify(moduleRequest)}` +
297+
`\ncssModules["${exposedName}"] = ${styleVar}`
298+
)
299+
}
300+
301+
// these are built-in query parameters so should be ignored
302+
// if the user happen to add them as attrs
303+
const ignoreList = ['id', 'index', 'src', 'type', 'lang', 'module']
304+
305+
function attrsToQuery(
306+
attrs: SFCBlock['attrs'],
307+
langFallback?: string,
308+
forceLangFallback = false
309+
): string {
310+
let query = ``
311+
for (const name in attrs) {
312+
const value = attrs[name]
313+
if (!ignoreList.includes(name)) {
314+
query += `&${qs.escape(name)}${
315+
value ? `=${qs.escape(String(value))}` : ``
316+
}`
317+
}
318+
}
319+
if (langFallback || attrs.lang) {
320+
query +=
321+
`lang` in attrs
322+
? forceLangFallback
323+
? `&lang.${langFallback}`
324+
: `&lang.${attrs.lang}`
325+
: `&lang.${langFallback}`
326+
}
327+
return query
328+
}

‎packages/plugin-vue/src/script.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { compileScript, SFCDescriptor, SFCScriptBlock } from '@vue/compiler-sfc'
2+
import { ResolvedOptions } from '.'
3+
import { getTemplateCompilerOptions } from './template'
4+
5+
// ssr and non ssr builds would output different script content
6+
const clientCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
7+
const ssrCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()
8+
9+
export function getResolvedScript(
10+
descriptor: SFCDescriptor,
11+
isServer = false
12+
): SFCScriptBlock | null | undefined {
13+
return (isServer ? ssrCache : clientCache).get(descriptor)
14+
}
15+
16+
export function setResolvedScript(
17+
descriptor: SFCDescriptor,
18+
script: SFCScriptBlock,
19+
isServer = false
20+
) {
21+
;(isServer ? ssrCache : clientCache).set(descriptor, script)
22+
}
23+
24+
export function resolveScript(
25+
descriptor: SFCDescriptor,
26+
options: ResolvedOptions
27+
) {
28+
if (!descriptor.script && !descriptor.scriptSetup) {
29+
return null
30+
}
31+
32+
const cacheToUse = options.ssr ? ssrCache : clientCache
33+
const cached = cacheToUse.get(descriptor)
34+
if (cached) {
35+
return cached
36+
}
37+
38+
let resolved: SFCScriptBlock | null = null
39+
40+
resolved = compileScript(descriptor, {
41+
id: descriptor.id,
42+
isProd: options.isProduction,
43+
inlineTemplate: !options.devServer,
44+
templateOptions: getTemplateCompilerOptions(descriptor, options)
45+
})
46+
47+
cacheToUse.set(descriptor, resolved)
48+
return resolved
49+
}

‎packages/plugin-vue/src/style.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { compileStyleAsync, SFCDescriptor } from '@vue/compiler-sfc'
2+
import { TransformPluginContext } from 'rollup'
3+
import { ResolvedOptions } from '.'
4+
5+
export async function transformStyle(
6+
code: string,
7+
descriptor: SFCDescriptor,
8+
index: number,
9+
options: ResolvedOptions,
10+
pluginContext: TransformPluginContext
11+
) {
12+
const block = descriptor.styles[index]
13+
// vite already handles pre-processors and CSS module so this is only
14+
// applying SFC-specific transforms like scoped mode and CSS vars rewrite (v-bind(var))
15+
const result = await compileStyleAsync({
16+
filename: descriptor.filename,
17+
id: `data-v-${descriptor.id}`,
18+
isProd: options.isProduction,
19+
source: code,
20+
scoped: block.scoped
21+
})
22+
23+
if (result.errors.length) {
24+
result.errors.forEach((error: any) => {
25+
if (error.line && error.column) {
26+
error.loc = {
27+
file: descriptor.filename,
28+
line: error.line + block.loc.start.line,
29+
column: error.column
30+
}
31+
}
32+
pluginContext.error(error)
33+
})
34+
return null
35+
}
36+
37+
return {
38+
code: result.code,
39+
map: result.map as any
40+
}
41+
}

‎packages/plugin-vue/src/template.ts

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {
2+
compileTemplate,
3+
SFCDescriptor,
4+
SFCTemplateCompileOptions
5+
} from '@vue/compiler-sfc'
6+
import { PluginContext, TransformPluginContext } from 'rollup'
7+
import { ResolvedOptions } from '.'
8+
import { getResolvedScript } from './script'
9+
import { createRollupError } from './utils/error'
10+
11+
export function transformTemplateAsModule(
12+
code: string,
13+
descriptor: SFCDescriptor,
14+
options: ResolvedOptions,
15+
pluginContext: TransformPluginContext
16+
) {
17+
const result = compile(code, descriptor, options, pluginContext)
18+
19+
let returnCode = result.code
20+
if (options.devServer) {
21+
returnCode += `\nimport.meta.hot.accept(({ render }) => {
22+
__VUE_HMR_RUNTIME__.rerender(${JSON.stringify(descriptor.id)}, render)
23+
})`
24+
}
25+
26+
return {
27+
code: returnCode,
28+
map: result.map as any
29+
}
30+
}
31+
32+
/**
33+
* transform the template directly in the main SFC module
34+
*/
35+
export function transformTemplateInMain(
36+
code: string,
37+
descriptor: SFCDescriptor,
38+
options: ResolvedOptions,
39+
pluginContext: PluginContext
40+
) {
41+
const result = compile(code, descriptor, options, pluginContext)
42+
return {
43+
...result,
44+
code: result.code.replace(
45+
/\nexport (function|const) (render|ssrRender)/,
46+
'\n$1 _sfc_$2'
47+
)
48+
}
49+
}
50+
51+
export function compile(
52+
code: string,
53+
descriptor: SFCDescriptor,
54+
options: ResolvedOptions,
55+
pluginContext: PluginContext
56+
) {
57+
const filename = descriptor.filename
58+
const result = compileTemplate({
59+
...getTemplateCompilerOptions(descriptor, options)!,
60+
source: code
61+
})
62+
63+
if (result.errors.length) {
64+
result.errors.forEach((error) =>
65+
pluginContext.error(
66+
typeof error === 'string'
67+
? { id: filename, message: error }
68+
: createRollupError(filename, error)
69+
)
70+
)
71+
}
72+
73+
if (result.tips.length) {
74+
result.tips.forEach((tip) =>
75+
pluginContext.warn({
76+
id: filename,
77+
message: tip
78+
})
79+
)
80+
}
81+
82+
return result
83+
}
84+
85+
export function getTemplateCompilerOptions(
86+
descriptor: SFCDescriptor,
87+
options: ResolvedOptions
88+
): Omit<SFCTemplateCompileOptions, 'source'> | undefined {
89+
const block = descriptor.template
90+
if (!block) {
91+
return
92+
}
93+
const resolvedScript = getResolvedScript(descriptor, options.ssr)
94+
const hasScoped = descriptor.styles.some((s) => s.scoped)
95+
return {
96+
...options.template,
97+
id: descriptor.id,
98+
scoped: hasScoped,
99+
isProd: options.isProduction,
100+
filename: descriptor.filename,
101+
inMap: block.src ? undefined : block.map,
102+
ssr: options.ssr,
103+
ssrCssVars: descriptor.cssVars,
104+
transformAssetUrls: options.template?.transformAssetUrls,
105+
compilerOptions: {
106+
...options.template?.compilerOptions,
107+
scopeId: hasScoped ? `data-v-${descriptor.id}` : undefined,
108+
bindingMetadata: resolvedScript ? resolvedScript.bindings : undefined
109+
}
110+
}
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { SFCDescriptor } from '@vue/compiler-sfc'
2+
3+
const cache = new Map<string, SFCDescriptor>()
4+
const prevCache = new Map<string, SFCDescriptor | undefined>()
5+
6+
export function setDescriptor(filename: string, entry: SFCDescriptor) {
7+
cache.set(filename, entry)
8+
}
9+
10+
export function getPrevDescriptor(filename: string) {
11+
return prevCache.get(filename)
12+
}
13+
14+
export function setPrevDescriptor(filename: string, entry: SFCDescriptor) {
15+
prevCache.set(filename, entry)
16+
}
17+
18+
export function getDescriptor(filename: string) {
19+
if (cache.has(filename)) {
20+
return cache.get(filename)!
21+
}
22+
throw new Error(
23+
`${filename} has no corresponding SFC entry in the cache. ` +
24+
`This is a @vitejs/plugin-vue internal error, please open an issue.`
25+
)
26+
}
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { CompilerError } from '@vue/compiler-sfc'
2+
import { RollupError } from 'rollup'
3+
4+
export function createRollupError(
5+
id: string,
6+
error: CompilerError | SyntaxError
7+
): RollupError {
8+
if ('code' in error) {
9+
return {
10+
id,
11+
plugin: 'vue',
12+
message: error.message,
13+
parserError: error,
14+
loc: error.loc
15+
? {
16+
file: id,
17+
line: error.loc.start.line,
18+
column: error.loc.start.column
19+
}
20+
: undefined
21+
}
22+
} else {
23+
return {
24+
id,
25+
plugin: 'vue',
26+
message: error.message,
27+
parserError: error
28+
}
29+
}
30+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import qs from 'querystring'
2+
3+
export interface VueQuery {
4+
vue?: boolean
5+
type?: 'script' | 'template' | 'style' | 'custom'
6+
src?: string
7+
index?: number
8+
lang?: string
9+
}
10+
11+
export function parseVueRequest(id: string) {
12+
const [filename, rawQuery] = id.split(`?`, 2)
13+
const query = qs.parse(rawQuery) as VueQuery
14+
if (query.vue != null) {
15+
query.vue = true
16+
}
17+
if (query.index) {
18+
query.index = Number(query.index)
19+
}
20+
return {
21+
filename,
22+
query
23+
}
24+
}

‎packages/plugin-vue/tsconfig.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"include": ["src"],
3+
"exclude": ["**/*.spec.ts"],
4+
"compilerOptions": {
5+
"outDir": "dist",
6+
"target": "ES2019",
7+
"module": "commonjs",
8+
"moduleResolution": "node",
9+
"strict": true,
10+
"declaration": true,
11+
"sourceMap": true,
12+
"noUnusedLocals": true,
13+
"esModuleInterop": true,
14+
"baseUrl": ".",
15+
"paths": {
16+
// vite typings uses custom paths that is patched into relative paths during build
17+
// this is a shim that makes even dev-time vite typings work for plugin-vue
18+
"types/*": ["../vite/types/*"]
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)
Please sign in to comment.