1 | const path = require('path')
|
2 | const hash = require('hash-sum')
|
3 | const qs = require('querystring')
|
4 | const plugin = require('./plugin')
|
5 | const selectBlock = require('./select')
|
6 | const loaderUtils = require('loader-utils')
|
7 | const { attrsToQuery } = require('./codegen/utils')
|
8 | const { parse } = require('@vue/component-compiler-utils')
|
9 | const genStylesCode = require('./codegen/styleInjection')
|
10 | const { genHotReloadCode } = require('./codegen/hotReload')
|
11 | const genCustomBlocksCode = require('./codegen/customBlocks')
|
12 | const componentNormalizerPath = require.resolve('./runtime/componentNormalizer')
|
13 | const { NS } = require('./plugin')
|
14 |
|
15 | let errorEmitted = false
|
16 |
|
17 | function loadTemplateCompiler (loaderContext) {
|
18 | try {
|
19 | return require('vue-template-compiler')
|
20 | } catch (e) {
|
21 | if (/version mismatch/.test(e.toString())) {
|
22 | loaderContext.emitError(e)
|
23 | } else {
|
24 | loaderContext.emitError(new Error(
|
25 | `[vue-loader] vue-template-compiler must be installed as a peer dependency, ` +
|
26 | `or a compatible compiler implementation must be passed via options.`
|
27 | ))
|
28 | }
|
29 | }
|
30 | }
|
31 |
|
32 | module.exports = function (source) {
|
33 | const loaderContext = this
|
34 |
|
35 | if (!errorEmitted && !loaderContext['thread-loader'] && !loaderContext[NS]) {
|
36 | loaderContext.emitError(new Error(
|
37 | `vue-loader was used without the corresponding plugin. ` +
|
38 | `Make sure to include VueLoaderPlugin in your webpack config.`
|
39 | ))
|
40 | errorEmitted = true
|
41 | }
|
42 |
|
43 | const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r)
|
44 |
|
45 | const {
|
46 | target,
|
47 | request,
|
48 | minimize,
|
49 | sourceMap,
|
50 | rootContext,
|
51 | resourcePath,
|
52 | resourceQuery
|
53 | } = loaderContext
|
54 |
|
55 | const rawQuery = resourceQuery.slice(1)
|
56 | const inheritQuery = `&${rawQuery}`
|
57 | const incomingQuery = qs.parse(rawQuery)
|
58 | const options = loaderUtils.getOptions(loaderContext) || {}
|
59 |
|
60 | const isServer = target === 'node'
|
61 | const isShadow = !!options.shadowMode
|
62 | const isProduction = options.productionMode || minimize || process.env.NODE_ENV === 'production'
|
63 | const filename = path.basename(resourcePath)
|
64 | const context = rootContext || process.cwd()
|
65 | const sourceRoot = path.dirname(path.relative(context, resourcePath))
|
66 |
|
67 | const descriptor = parse({
|
68 | source,
|
69 | compiler: options.compiler || loadTemplateCompiler(loaderContext),
|
70 | filename,
|
71 | sourceRoot,
|
72 | needMap: sourceMap
|
73 | })
|
74 |
|
75 |
|
76 |
|
77 |
|
78 | if (incomingQuery.type) {
|
79 | return selectBlock(
|
80 | descriptor,
|
81 | loaderContext,
|
82 | incomingQuery,
|
83 | !!options.appendExtension
|
84 | )
|
85 | }
|
86 |
|
87 |
|
88 | const rawShortFilePath = path
|
89 | .relative(context, resourcePath)
|
90 | .replace(/^(\.\.[\/\\])+/, '')
|
91 |
|
92 | const shortFilePath = rawShortFilePath.replace(/\\/g, '/') + resourceQuery
|
93 |
|
94 | const id = hash(
|
95 | isProduction
|
96 | ? (shortFilePath + '\n' + source)
|
97 | : shortFilePath
|
98 | )
|
99 |
|
100 |
|
101 | const hasScoped = descriptor.styles.some(s => s.scoped)
|
102 | const hasFunctional = descriptor.template && descriptor.template.attrs.functional
|
103 | const needsHotReload = (
|
104 | !isServer &&
|
105 | !isProduction &&
|
106 | (descriptor.script || descriptor.template) &&
|
107 | options.hotReload !== false
|
108 | )
|
109 |
|
110 |
|
111 | let templateImport = `var render, staticRenderFns`
|
112 | let templateRequest
|
113 | if (descriptor.template) {
|
114 | const src = descriptor.template.src || resourcePath
|
115 | const idQuery = `&id=${id}`
|
116 | const scopedQuery = hasScoped ? `&scoped=true` : ``
|
117 | const attrsQuery = attrsToQuery(descriptor.template.attrs)
|
118 | const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
|
119 | const request = templateRequest = stringifyRequest(src + query)
|
120 | templateImport = `import { render, staticRenderFns } from ${request}`
|
121 | }
|
122 |
|
123 |
|
124 | let scriptImport = `var script = {}`
|
125 | if (descriptor.script) {
|
126 | const src = descriptor.script.src || resourcePath
|
127 | const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
|
128 | const query = `?vue&type=script${attrsQuery}${inheritQuery}`
|
129 | const request = stringifyRequest(src + query)
|
130 | scriptImport = (
|
131 | `import script from ${request}\n` +
|
132 | `export * from ${request}`
|
133 | )
|
134 | }
|
135 |
|
136 |
|
137 | let stylesCode = ``
|
138 | if (descriptor.styles.length) {
|
139 | stylesCode = genStylesCode(
|
140 | loaderContext,
|
141 | descriptor.styles,
|
142 | id,
|
143 | resourcePath,
|
144 | stringifyRequest,
|
145 | needsHotReload,
|
146 | isServer || isShadow
|
147 | )
|
148 | }
|
149 |
|
150 | let code = `
|
151 | ${templateImport}
|
152 | ${scriptImport}
|
153 | ${stylesCode}
|
154 |
|
155 | /* normalize component */
|
156 | import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
|
157 | var component = normalizer(
|
158 | script,
|
159 | render,
|
160 | staticRenderFns,
|
161 | ${hasFunctional ? `true` : `false`},
|
162 | ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
|
163 | ${hasScoped ? JSON.stringify(id) : `null`},
|
164 | ${isServer ? JSON.stringify(hash(request)) : `null`}
|
165 | ${isShadow ? `,true` : ``}
|
166 | )
|
167 | `.trim() + `\n`
|
168 |
|
169 | if (descriptor.customBlocks && descriptor.customBlocks.length) {
|
170 | code += genCustomBlocksCode(
|
171 | descriptor.customBlocks,
|
172 | resourcePath,
|
173 | resourceQuery,
|
174 | stringifyRequest
|
175 | )
|
176 | }
|
177 |
|
178 | if (needsHotReload) {
|
179 | code += `\n` + genHotReloadCode(id, hasFunctional, templateRequest)
|
180 | }
|
181 |
|
182 |
|
183 | if (!isProduction) {
|
184 |
|
185 |
|
186 | code += `\ncomponent.options.__file = ${JSON.stringify(rawShortFilePath.replace(/\\/g, '/'))}`
|
187 | } else if (options.exposeFilename) {
|
188 |
|
189 |
|
190 | code += `\ncomponent.options.__file = ${JSON.stringify(filename)}`
|
191 | }
|
192 |
|
193 | code += `\nexport default component.exports`
|
194 | return code
|
195 | }
|
196 |
|
197 | module.exports.VueLoaderPlugin = plugin
|