You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
6.0 KiB

4 years ago
  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. let errorEmitted = false
  15. function loadTemplateCompiler (loaderContext) {
  16. try {
  17. return require('vue-template-compiler')
  18. } catch (e) {
  19. if (/version mismatch/.test(e.toString())) {
  20. loaderContext.emitError(e)
  21. } else {
  22. loaderContext.emitError(new Error(
  23. `[vue-loader] vue-template-compiler must be installed as a peer dependency, ` +
  24. `or a compatible compiler implementation must be passed via options.`
  25. ))
  26. }
  27. }
  28. }
  29. module.exports = function (source) {
  30. const loaderContext = this
  31. if (!errorEmitted && !loaderContext['thread-loader'] && !loaderContext[NS]) {
  32. loaderContext.emitError(new Error(
  33. `vue-loader was used without the corresponding plugin. ` +
  34. `Make sure to include VueLoaderPlugin in your webpack config.`
  35. ))
  36. errorEmitted = true
  37. }
  38. const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r)
  39. const {
  40. target,
  41. request,
  42. minimize,
  43. sourceMap,
  44. rootContext,
  45. resourcePath,
  46. resourceQuery
  47. } = loaderContext
  48. const rawQuery = resourceQuery.slice(1)
  49. const inheritQuery = `&${rawQuery}`
  50. const incomingQuery = qs.parse(rawQuery)
  51. const options = loaderUtils.getOptions(loaderContext) || {}
  52. const isServer = target === 'node'
  53. const isShadow = !!options.shadowMode
  54. const isProduction = options.productionMode || minimize || process.env.NODE_ENV === 'production'
  55. const filename = path.basename(resourcePath)
  56. const context = rootContext || process.cwd()
  57. const sourceRoot = path.dirname(path.relative(context, resourcePath))
  58. const descriptor = parse({
  59. source,
  60. compiler: options.compiler || loadTemplateCompiler(loaderContext),
  61. filename,
  62. sourceRoot,
  63. needMap: sourceMap
  64. })
  65. // if the query has a type field, this is a language block request
  66. // e.g. foo.vue?type=template&id=xxxxx
  67. // and we will return early
  68. if (incomingQuery.type) {
  69. return selectBlock(
  70. descriptor,
  71. loaderContext,
  72. incomingQuery,
  73. !!options.appendExtension
  74. )
  75. }
  76. // module id for scoped CSS & hot-reload
  77. const rawShortFilePath = path
  78. .relative(context, resourcePath)
  79. .replace(/^(\.\.[\/\\])+/, '')
  80. const shortFilePath = rawShortFilePath.replace(/\\/g, '/') + resourceQuery
  81. const id = hash(
  82. isProduction
  83. ? (shortFilePath + '\n' + source)
  84. : shortFilePath
  85. )
  86. // feature information
  87. const hasScoped = descriptor.styles.some(s => s.scoped)
  88. const hasFunctional = descriptor.template && descriptor.template.attrs.functional
  89. const needsHotReload = (
  90. !isServer &&
  91. !isProduction &&
  92. (descriptor.script || descriptor.template) &&
  93. options.hotReload !== false
  94. )
  95. // template
  96. let templateImport = `var render, staticRenderFns`
  97. let templateRequest
  98. if (descriptor.template) {
  99. const src = descriptor.template.src || resourcePath
  100. const idQuery = `&id=${id}`
  101. const scopedQuery = hasScoped ? `&scoped=true` : ``
  102. const attrsQuery = attrsToQuery(descriptor.template.attrs)
  103. const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
  104. const request = templateRequest = stringifyRequest(src + query)
  105. templateImport = `import { render, staticRenderFns } from ${request}`
  106. }
  107. // script
  108. let scriptImport = `var script = {}`
  109. if (descriptor.script) {
  110. const src = descriptor.script.src || resourcePath
  111. const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
  112. const query = `?vue&type=script${attrsQuery}${inheritQuery}`
  113. const request = stringifyRequest(src + query)
  114. scriptImport = (
  115. `import script from ${request}\n` +
  116. `export * from ${request}` // support named exports
  117. )
  118. }
  119. // styles
  120. let stylesCode = ``
  121. if (descriptor.styles.length) {
  122. stylesCode = genStylesCode(
  123. loaderContext,
  124. descriptor.styles,
  125. id,
  126. resourcePath,
  127. stringifyRequest,
  128. needsHotReload,
  129. isServer || isShadow // needs explicit injection?
  130. )
  131. }
  132. let code = `
  133. ${templateImport}
  134. ${scriptImport}
  135. ${stylesCode}
  136. /* normalize component */
  137. import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
  138. var component = normalizer(
  139. script,
  140. render,
  141. staticRenderFns,
  142. ${hasFunctional ? `true` : `false`},
  143. ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
  144. ${hasScoped ? JSON.stringify(id) : `null`},
  145. ${isServer ? JSON.stringify(hash(request)) : `null`}
  146. ${isShadow ? `,true` : ``}
  147. )
  148. `.trim() + `\n`
  149. if (descriptor.customBlocks && descriptor.customBlocks.length) {
  150. code += genCustomBlocksCode(
  151. descriptor.customBlocks,
  152. resourcePath,
  153. resourceQuery,
  154. stringifyRequest
  155. )
  156. }
  157. if (needsHotReload) {
  158. code += `\n` + genHotReloadCode(id, hasFunctional, templateRequest)
  159. }
  160. // Expose filename. This is used by the devtools and Vue runtime warnings.
  161. if (!isProduction) {
  162. // Expose the file's full path in development, so that it can be opened
  163. // from the devtools.
  164. code += `\ncomponent.options.__file = ${JSON.stringify(rawShortFilePath.replace(/\\/g, '/'))}`
  165. } else if (options.exposeFilename) {
  166. // Libraies can opt-in to expose their components' filenames in production builds.
  167. // For security reasons, only expose the file's basename in production.
  168. code += `\ncomponent.options.__file = ${JSON.stringify(filename)}`
  169. }
  170. code += `\nexport default component.exports`
  171. // console.log(code)
  172. return code
  173. }
  174. module.exports.VueLoaderPlugin = plugin