index.js 7.1 KB

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