index.js 6.1 KB

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