-
Notifications
You must be signed in to change notification settings - Fork 915
/
Copy pathindex.ts
313 lines (278 loc) · 9.67 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
import webpack = require('webpack')
import * as path from 'path'
import * as qs from 'querystring'
import * as loaderUtils from 'loader-utils'
import hash = require('hash-sum')
import { parse } from 'vue/compiler-sfc'
import type {
TemplateCompiler,
CompilerOptions,
SFCBlock,
SFCTemplateCompileOptions,
SFCScriptCompileOptions,
} from 'vue/compiler-sfc'
import { selectBlock } from './select'
import { genHotReloadCode } from './hotReload'
import { genCSSModulesCode } from './cssModules'
import { formatError } from './formatError'
import VueLoaderPlugin from './plugin'
import { canInlineTemplate } from './resolveScript'
import { setDescriptor } from './descriptorCache'
export { VueLoaderPlugin }
export interface VueLoaderOptions {
// https://babeljs.io/docs/en/next/babel-parser#plugins
babelParserPlugins?: SFCScriptCompileOptions['babelParserPlugins']
transformAssetUrls?: SFCTemplateCompileOptions['transformAssetUrls']
compiler?: TemplateCompiler | string
compilerOptions?: CompilerOptions
reactivityTransform?: boolean
customElement?: boolean | RegExp
hotReload?: boolean
exposeFilename?: boolean
appendExtension?: boolean
enableTsInTemplate?: boolean
isServerBuild?: boolean
}
let errorEmitted = false
const exportHelperPath = JSON.stringify(require.resolve('./exportHelper'))
export default function loader(
this: webpack.loader.LoaderContext,
source: string
) {
const loaderContext = this
// check if plugin is installed
if (
!errorEmitted &&
!(loaderContext as any)['thread-loader'] &&
!(loaderContext as any)[VueLoaderPlugin.NS]
) {
loaderContext.emitError(
new Error(
`vue-loader was used without the corresponding plugin. ` +
`Make sure to include VueLoaderPlugin in your webpack config.`
)
)
errorEmitted = true
}
const stringifyRequest = (r: string) =>
loaderUtils.stringifyRequest(loaderContext, r)
const {
mode,
target,
sourceMap,
rootContext,
resourcePath,
resourceQuery = '',
} = loaderContext
const rawQuery = resourceQuery.slice(1)
const incomingQuery = qs.parse(rawQuery)
const options = (loaderUtils.getOptions(loaderContext) ||
{}) as VueLoaderOptions
const isServer = options.isServerBuild ?? target === 'node'
const isProduction =
mode === 'production' || process.env.NODE_ENV === 'production'
const filename = resourcePath.replace(/\?.*$/, '')
const { descriptor, errors } = parse(source, {
filename,
sourceMap,
})
const asCustomElement =
typeof options.customElement === 'boolean'
? options.customElement
: (options.customElement || /\.ce\.vue$/).test(filename)
// cache descriptor
setDescriptor(filename, descriptor)
if (errors.length) {
errors.forEach((err) => {
formatError(err, source, resourcePath)
loaderContext.emitError(err)
})
return ``
}
// module id for scoped CSS & hot-reload
const rawShortFilePath = path
.relative(rootContext || process.cwd(), filename)
.replace(/^(\.\.[\/\\])+/, '')
const shortFilePath = rawShortFilePath.replace(/\\/g, '/')
const id = hash(
isProduction
? shortFilePath + '\n' + source.replace(/\r\n/g, '\n')
: shortFilePath
)
// if the query has a type field, this is a language block request
// e.g. foo.vue?type=template&id=xxxxx
// and we will return early
if (incomingQuery.type) {
return selectBlock(
descriptor,
id,
options,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
// feature information
const hasScoped = descriptor.styles.some((s) => s.scoped)
const needsHotReload =
!isServer &&
!isProduction &&
!!(descriptor.script || descriptor.scriptSetup || descriptor.template) &&
options.hotReload !== false
// extra properties to attach to the script object
// we need to do this in a tree-shaking friendly manner
const propsToAttach: [string, string][] = []
// 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}`
const scriptRequest = stringifyRequest(src + query)
scriptImport =
`import script from ${scriptRequest}\n` +
// support named exports
`export * from ${scriptRequest}`
}
// template
let templateImport = ``
let templateRequest
const renderFnName = isServer ? `ssrRender` : `render`
const useInlineTemplate = canInlineTemplate(descriptor, isProduction)
if (descriptor.template && !useInlineTemplate) {
const src = descriptor.template.src || resourcePath
const idQuery = `&id=${id}`
const scopedQuery = hasScoped ? `&scoped=true` : ``
const attrsQuery = attrsToQuery(descriptor.template.attrs)
const tsQuery =
options.enableTsInTemplate !== false && isTS ? `&ts=true` : ``
const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}`
templateRequest = stringifyRequest(src + query)
templateImport = `import { ${renderFnName} } from ${templateRequest}`
propsToAttach.push([renderFnName, renderFnName])
}
// styles
let stylesCode = ``
let hasCSSModules = false
const nonWhitespaceRE = /\S+/
if (descriptor.styles.length) {
descriptor.styles
.filter((style) => style.src || nonWhitespaceRE.test(style.content))
.forEach((style, i) => {
const src = style.src || resourcePath
const attrsQuery = attrsToQuery(style.attrs, 'css')
// make sure to only pass id when necessary so that we don't inject
// duplicate tags when multiple components import the same css file
const idQuery = !style.src || style.scoped ? `&id=${id}` : ``
const inlineQuery = asCustomElement ? `&inline` : ``
const query = `?vue&type=style&index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}`
const styleRequest = stringifyRequest(src + query)
if (style.module) {
if (asCustomElement) {
loaderContext.emitError(
`<style module> is not supported in custom element mode.`
)
}
if (!hasCSSModules) {
stylesCode += `\nconst cssModules = {}`
propsToAttach.push([`__cssModules`, `cssModules`])
hasCSSModules = true
}
stylesCode += genCSSModulesCode(
id,
i,
styleRequest,
style.module,
needsHotReload
)
} else {
if (asCustomElement) {
stylesCode += `\nimport _style_${i} from ${styleRequest}`
} else {
stylesCode += `\nimport ${styleRequest}`
}
}
// TODO SSR critical CSS collection
})
if (asCustomElement) {
propsToAttach.push([
`styles`,
`[${descriptor.styles.map((_, i) => `_style_${i}`)}]`,
])
}
}
let code = [templateImport, scriptImport, stylesCode]
.filter(Boolean)
.join('\n')
// attach scope Id for runtime use
if (hasScoped) {
propsToAttach.push([`__scopeId`, `"data-v-${id}"`])
}
// Expose filename. This is used by the devtools and Vue runtime warnings.
if (!isProduction) {
// Expose the file's full path in development, so that it can be opened
// from the devtools.
propsToAttach.push([
`__file`,
JSON.stringify(rawShortFilePath.replace(/\\/g, '/')),
])
} else if (options.exposeFilename) {
// Libraries can opt-in to expose their components' filenames in production builds.
// For security reasons, only expose the file's basename in production.
propsToAttach.push([`__file`, JSON.stringify(path.basename(resourcePath))])
}
// custom blocks
if (descriptor.customBlocks && descriptor.customBlocks.length) {
code += `\n/* custom blocks */\n`
code +=
descriptor.customBlocks
.map((block, i) => {
const src = block.attrs.src || resourcePath
const attrsQuery = attrsToQuery(block.attrs)
const blockTypeQuery = `&blockType=${qs.escape(block.type)}`
const issuerQuery = block.attrs.src
? `&issuerPath=${qs.escape(resourcePath)}`
: ''
const query = `?vue&type=custom&index=${i}${blockTypeQuery}${issuerQuery}${attrsQuery}${resourceQuery}`
return (
`import block${i} from ${stringifyRequest(src + query)}\n` +
`if (typeof block${i} === 'function') block${i}(script)`
)
})
.join(`\n`) + `\n`
}
// finalize
if (!propsToAttach.length) {
code += `\n\nconst __exports__ = script;`
} else {
code += `\n\nimport exportComponent from ${exportHelperPath}`
code += `\nconst __exports__ = /*#__PURE__*/exportComponent(script, [${propsToAttach
.map(([key, val]) => `['${key}',${val}]`)
.join(',')}])`
}
if (needsHotReload) {
code += genHotReloadCode(id, templateRequest)
}
code += `\n\nexport default __exports__`
return code
}
// these are built-in query parameters so should be ignored
// if the user happen to add them as attrs
const ignoreList = ['id', 'index', 'src', 'type']
function attrsToQuery(attrs: SFCBlock['attrs'], langFallback?: string): string {
let query = ``
for (const name in attrs) {
const value = attrs[name]
if (!ignoreList.includes(name)) {
query += `&${qs.escape(name)}=${value ? qs.escape(String(value)) : ``}`
}
}
if (langFallback && !(`lang` in attrs)) {
query += `&lang=${langFallback}`
}
return query
}