loader.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. var path = require('path')
  2. var parse = require('./parser')
  3. var genId = require('./utils/gen-id')
  4. var normalize = require('./utils/normalize')
  5. var loaderUtils = require('loader-utils')
  6. var querystring = require('querystring')
  7. // internal lib loaders
  8. var selectorPath = normalize.lib('selector')
  9. var styleCompilerPath = normalize.lib('style-compiler/index')
  10. var templateCompilerPath = normalize.lib('template-compiler/index')
  11. var templatePreprocessorPath = normalize.lib('template-compiler/preprocessor')
  12. var componentNormalizerPath = normalize.lib('component-normalizer')
  13. // dep loaders
  14. var styleLoaderPath = normalize.dep('vue-style-loader')
  15. var hotReloadAPIPath = normalize.dep('vue-hot-reload-api')
  16. var hasBabel = false
  17. try {
  18. hasBabel = !!require('babel-loader')
  19. } catch (e) {}
  20. var hasBuble = false
  21. try {
  22. hasBuble = !!require('buble-loader')
  23. } catch (e) {}
  24. var rewriterInjectRE = /\b(css(?:-loader)?(?:\?[^!]+)?)(?:!|$)/
  25. var defaultLang = {
  26. template: 'html',
  27. styles: 'css',
  28. script: 'js'
  29. }
  30. // When extracting parts from the source vue file, we want to apply the
  31. // loaders chained before vue-loader, but exclude some loaders that simply
  32. // produces side effects such as linting.
  33. function getRawRequest (context, excludedPreLoaders) {
  34. excludedPreLoaders = excludedPreLoaders || /eslint-loader/
  35. return loaderUtils.getRemainingRequest({
  36. resource: context.resource,
  37. loaderIndex: context.loaderIndex,
  38. loaders: context.loaders.filter(loader => !excludedPreLoaders.test(loader.path))
  39. })
  40. }
  41. module.exports = function (content) {
  42. this.cacheable()
  43. var isServer = this.target === 'node'
  44. var isProduction = this.minimize || process.env.NODE_ENV === 'production'
  45. var loaderContext = this
  46. var query = loaderUtils.getOptions(this) || {}
  47. var options = this.options.__vueOptions__ = Object.assign({}, this.options.vue, this.vue, query)
  48. var rawRequest = getRawRequest(this, options.excludedPreLoaders)
  49. var filePath = this.resourcePath
  50. var fileName = path.basename(filePath)
  51. var context = (this._compiler && this._compiler.context) || this.options.context || process.cwd()
  52. var moduleId = 'data-v-' + genId(filePath, context, options.hashKey)
  53. var cssLoaderOptions = ''
  54. if (!isProduction && this.sourceMap && options.cssSourceMap !== false) {
  55. cssLoaderOptions += '?sourceMap'
  56. }
  57. if (isProduction) {
  58. cssLoaderOptions += (cssLoaderOptions ? '&' : '?') + 'minimize'
  59. }
  60. var bubleOptions = hasBuble && options.buble
  61. ? '?' + JSON.stringify(options.buble)
  62. : ''
  63. var templateCompilerOptions = '?' + JSON.stringify({
  64. id: moduleId,
  65. transformToRequire: options.transformToRequire,
  66. preserveWhitespace: options.preserveWhitespace,
  67. buble: options.buble,
  68. // only pass compilerModules if it's a path string
  69. compilerModules: typeof options.compilerModules === 'string'
  70. ? options.compilerModules
  71. : undefined
  72. })
  73. var defaultLoaders = {
  74. html: templateCompilerPath + templateCompilerOptions,
  75. css: styleLoaderPath + '!' + 'css-loader' + cssLoaderOptions,
  76. js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? 'babel-loader' : ''
  77. }
  78. // check if there are custom loaders specified via
  79. // webpack config, otherwise use defaults
  80. var loaders = Object.assign({}, defaultLoaders, options.loaders)
  81. var preLoaders = options.preLoaders || {}
  82. var postLoaders = options.postLoaders || {}
  83. function getRequire (type, part, index, scoped) {
  84. return 'require(' +
  85. getRequireString(type, part, index, scoped) +
  86. ')'
  87. }
  88. function getRequireString (type, part, index, scoped) {
  89. return loaderUtils.stringifyRequest(loaderContext,
  90. // disable all configuration loaders
  91. '!!' +
  92. // get loader string for pre-processors
  93. getLoaderString(type, part, index, scoped) +
  94. // select the corresponding part from the vue file
  95. getSelectorString(type, index || 0) +
  96. // the url to the actual vue file, including remaining requests
  97. rawRequest
  98. )
  99. }
  100. function getRequireForImport (type, impt, scoped) {
  101. return 'require(' +
  102. getRequireForImportString(type, impt, scoped) +
  103. ')'
  104. }
  105. function getRequireForImportString (type, impt, scoped) {
  106. return loaderUtils.stringifyRequest(loaderContext,
  107. '!!' +
  108. getLoaderString(type, impt, -1, scoped) +
  109. impt.src
  110. )
  111. }
  112. function addCssModulesToLoader (loader, part, index) {
  113. if (!part.module) return loader
  114. var option = options.cssModules || {}
  115. var DEFAULT_OPTIONS = {
  116. modules: true,
  117. importLoaders: true
  118. }
  119. var OPTIONS = {
  120. localIdentName: '[hash:base64]'
  121. }
  122. return loader.replace(/((?:^|!)css(?:-loader)?)(\?[^!]*)?/, function (m, $1, $2) {
  123. // $1: !css-loader
  124. // $2: ?a=b
  125. var query = loaderUtils.parseQuery($2 || '?')
  126. Object.assign(query, OPTIONS, option, DEFAULT_OPTIONS)
  127. if (index !== -1) {
  128. // Note:
  129. // Class name is generated according to its filename.
  130. // Different <style> tags in the same .vue file may generate same names.
  131. // Append `_[index]` to class name to avoid this.
  132. query.localIdentName += '_' + index
  133. }
  134. return $1 + '?' + JSON.stringify(query)
  135. })
  136. }
  137. function buildCustomBlockLoaderString (attrs) {
  138. var noSrcAttrs = Object.assign({}, attrs)
  139. delete noSrcAttrs.src
  140. var qs = querystring.stringify(noSrcAttrs)
  141. return qs ? '?' + qs : qs
  142. }
  143. // stringify an Array of loader objects
  144. function stringifyLoaders (loaders) {
  145. return loaders.map(function (obj) {
  146. return obj && typeof obj === 'object' && typeof obj.loader === 'string'
  147. ? obj.loader + (obj.options ? '?' + JSON.stringify(obj.options) : '')
  148. : obj
  149. }).join('!')
  150. }
  151. function getLoaderString (type, part, index, scoped) {
  152. var loader = getRawLoaderString(type, part, index, scoped)
  153. var lang = getLangString(type, part)
  154. if (preLoaders[lang]) {
  155. loader = loader + ensureBang(preLoaders[lang])
  156. }
  157. if (postLoaders[lang]) {
  158. loader = ensureBang(postLoaders[lang]) + loader
  159. }
  160. return loader
  161. }
  162. function getLangString (type, part) {
  163. if (type === 'script' || type === 'template' || type === 'styles') {
  164. return part.lang || defaultLang[type]
  165. } else {
  166. return type
  167. }
  168. }
  169. function getRawLoaderString (type, part, index, scoped) {
  170. var lang = part.lang || defaultLang[type]
  171. var loader = loaders[lang]
  172. var styleCompiler = ''
  173. if (type === 'styles') {
  174. styleCompiler = styleCompilerPath + '?' + JSON.stringify({
  175. id: moduleId,
  176. scoped: !!scoped,
  177. hasInlineConfig: !!query.postcss
  178. }) + '!'
  179. }
  180. var injectString = (type === 'script' && query.inject)
  181. ? 'inject-loader!'
  182. : ''
  183. if (loader != null) {
  184. if (Array.isArray(loader)) {
  185. loader = stringifyLoaders(loader)
  186. } else if (typeof loader === 'object') {
  187. loader = stringifyLoaders([loader])
  188. }
  189. if (type === 'styles') {
  190. // add css modules
  191. loader = addCssModulesToLoader(loader, part, index)
  192. // inject rewriter before css loader for extractTextPlugin use cases
  193. if (rewriterInjectRE.test(loader)) {
  194. loader = loader.replace(rewriterInjectRE, function (m, $1) {
  195. return ensureBang($1) + styleCompiler
  196. })
  197. } else {
  198. loader = ensureBang(loader) + styleCompiler
  199. }
  200. }
  201. // if user defines custom loaders for html, add template compiler to it
  202. if (type === 'template' && loader.indexOf(defaultLoaders.html) < 0) {
  203. loader = defaultLoaders.html + '!' + loader
  204. }
  205. return injectString + ensureBang(loader)
  206. } else {
  207. // unknown lang, infer the loader to be used
  208. switch (type) {
  209. case 'template':
  210. return defaultLoaders.html + '!' + templatePreprocessorPath + '?raw&engine=' + lang + '!'
  211. case 'styles':
  212. loader = addCssModulesToLoader(defaultLoaders.css, part, index)
  213. return loader + '!' + styleCompiler + ensureBang(ensureLoader(lang))
  214. case 'script':
  215. return injectString + ensureBang(ensureLoader(lang))
  216. default:
  217. loader = loaders[type]
  218. if (Array.isArray(loader)) {
  219. loader = stringifyLoaders(loader)
  220. }
  221. return ensureBang(loader + buildCustomBlockLoaderString(part.attrs))
  222. }
  223. }
  224. }
  225. // sass => sass-loader
  226. // sass-loader => sass-loader
  227. // sass?indentedsyntax!css => sass-loader?indentedSyntax!css-loader
  228. function ensureLoader (lang) {
  229. return lang.split('!').map(function (loader) {
  230. return loader.replace(/^([\w-]+)(\?.*)?/, function (_, name, query) {
  231. return (/-loader$/.test(name) ? name : (name + '-loader')) + (query || '')
  232. })
  233. }).join('!')
  234. }
  235. function getSelectorString (type, index) {
  236. return selectorPath +
  237. '?type=' + ((type === 'script' || type === 'template' || type === 'styles') ? type : 'customBlocks') +
  238. '&index=' + index + '!'
  239. }
  240. function ensureBang (loader) {
  241. if (loader.charAt(loader.length - 1) !== '!') {
  242. return loader + '!'
  243. } else {
  244. return loader
  245. }
  246. }
  247. var output = ''
  248. var parts = parse(content, fileName, this.sourceMap)
  249. var hasScoped = parts.styles.some(function (s) { return s.scoped })
  250. // css modules
  251. var cssModules
  252. // add requires for styles
  253. if (parts.styles.length) {
  254. output += '\n/* styles */\n'
  255. var hasModules = false
  256. parts.styles.forEach(function (style, i) {
  257. // require style
  258. var requireString = style.src
  259. ? getRequireForImport('styles', style, style.scoped)
  260. : getRequire('styles', style, i, style.scoped)
  261. var moduleName = (style.module === true) ? '$style' : style.module
  262. // setCssModule
  263. if (moduleName) {
  264. if (!cssModules) {
  265. cssModules = {}
  266. }
  267. if (!hasModules) {
  268. hasModules = true
  269. output += 'var cssModules = {}\n'
  270. }
  271. if (moduleName in cssModules) {
  272. loaderContext.emitError('CSS module name "' + moduleName + '" is not unique!')
  273. output += requireString
  274. } else {
  275. cssModules[moduleName] = true
  276. // `(vue-)style-loader` exposes the name-to-hash map directly
  277. // `css-loader` exposes it in `.locals`
  278. // add `.locals` if the user configured to not use style-loader.
  279. if (requireString.indexOf('style-loader') < 0) {
  280. requireString += '.locals'
  281. }
  282. output += 'cssModules["' + moduleName + '"] = ' + requireString + '\n'
  283. }
  284. } else {
  285. output += requireString + '\n'
  286. }
  287. })
  288. output += '\n'
  289. }
  290. // we require the component normalizer function, and call it like so:
  291. // normalizeComponent(
  292. // scriptExports,
  293. // compiledTemplate,
  294. // scopeId,
  295. // cssModules
  296. // )
  297. output += 'var Component = require(' +
  298. loaderUtils.stringifyRequest(loaderContext, '!' + componentNormalizerPath) +
  299. ')(\n'
  300. // <script>
  301. output += ' /* script */\n '
  302. var script = parts.script
  303. if (script) {
  304. output += script.src
  305. ? getRequireForImport('script', script)
  306. : getRequire('script', script)
  307. // inject loader interop
  308. if (query.inject) {
  309. output += '(injections)'
  310. }
  311. } else {
  312. output += 'null'
  313. }
  314. output += ',\n'
  315. // <template>
  316. output += ' /* template */\n '
  317. var template = parts.template
  318. if (template) {
  319. output += template.src
  320. ? getRequireForImport('template', template)
  321. : getRequire('template', template)
  322. } else {
  323. output += 'null'
  324. }
  325. output += ',\n'
  326. // scopeId
  327. output += ' /* scopeId */\n '
  328. output += (hasScoped ? JSON.stringify(moduleId) : 'null') + ',\n'
  329. // cssModules
  330. output += ' /* cssModules */\n '
  331. if (cssModules) {
  332. // inject style modules as computed properties
  333. output += 'cssModules'
  334. } else {
  335. output += 'null'
  336. }
  337. output += '\n'
  338. // close normalizeComponent call
  339. output += ')\n'
  340. // development-only code
  341. if (!isProduction) {
  342. // add filename in dev
  343. output += 'Component.options.__file = ' + JSON.stringify(filePath) + '\n'
  344. // check named exports
  345. output +=
  346. 'if (Component.esModule && Object.keys(Component.esModule).some(function (key) {' +
  347. 'return key !== "default" && key !== "__esModule"' +
  348. '})) {' +
  349. 'console.error("named exports are not supported in *.vue files.")' +
  350. '}\n'
  351. // check functional components used with templates
  352. if (template) {
  353. output +=
  354. 'if (Component.options.functional) {' +
  355. 'console.error("' +
  356. '[vue-loader] ' + fileName + ': functional components are not ' +
  357. 'supported with templates, they should use render functions.' +
  358. '")}\n'
  359. }
  360. }
  361. // add requires for customBlocks
  362. if (parts.customBlocks && parts.customBlocks.length) {
  363. var addedPrefix = false
  364. parts.customBlocks.forEach(function (customBlock, i) {
  365. if (loaders[customBlock.type]) {
  366. // require customBlock
  367. customBlock.src = customBlock.attrs.src
  368. var requireString = customBlock.src
  369. ? getRequireForImport(customBlock.type, customBlock)
  370. : getRequire(customBlock.type, customBlock, i)
  371. if (!addedPrefix) {
  372. output += '\n/* customBlocks */\n'
  373. addedPrefix = true
  374. }
  375. output +=
  376. 'var customBlock = ' + requireString + '\n' +
  377. 'if (typeof customBlock === "function") {' +
  378. 'customBlock(Component)' +
  379. '}\n'
  380. }
  381. })
  382. output += '\n'
  383. }
  384. if (!query.inject) {
  385. // hot reload
  386. if (
  387. !isServer &&
  388. !isProduction &&
  389. (parts.script || parts.template)
  390. ) {
  391. output +=
  392. '\n/* hot reload */\n' +
  393. 'if (module.hot) {(function () {\n' +
  394. ' var hotAPI = require("' + hotReloadAPIPath + '")\n' +
  395. ' hotAPI.install(require("vue"), false)\n' +
  396. ' if (!hotAPI.compatible) return\n' +
  397. ' module.hot.accept()\n' +
  398. ' if (!module.hot.data) {\n' +
  399. // initial insert
  400. ' hotAPI.createRecord("' + moduleId + '", Component.options)\n' +
  401. ' } else {\n'
  402. // update
  403. if (cssModules) {
  404. output +=
  405. ' if (module.hot.data.cssModules && JSON.stringify(module.hot.data.cssModules) !== JSON.stringify(cssModules)) {\n' +
  406. ' delete Component.options._Ctor\n' +
  407. ' }\n'
  408. }
  409. output +=
  410. ' hotAPI.reload("' + moduleId + '", Component.options)\n' +
  411. ' }\n'
  412. if (cssModules) {
  413. // save cssModules
  414. output +=
  415. ' module.hot.dispose(function (data) {\n' +
  416. ' data.cssModules = cssModules\n' +
  417. ' })\n'
  418. }
  419. output += '})()}\n'
  420. }
  421. // final export
  422. if (options.esModule) {
  423. output += '\nexports.__esModule = true;\nexports["default"] = Component.exports\n'
  424. } else {
  425. output += '\nmodule.exports = Component.exports\n'
  426. }
  427. } else {
  428. // inject-loader support
  429. output =
  430. '\n/* dependency injection */\n' +
  431. 'module.exports = function (injections) {\n' + output + '\n' +
  432. '\nreturn Component.exports\n}'
  433. }
  434. // done
  435. return output
  436. }