index.js 5.6 KB

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