UNPKG

mpvue-loader

Version:

mpvue single-file component loader for Webpack

656 lines (585 loc) 21.9 kB
var path = require('path') var hash = require('hash-sum') var parse = require('./parser') var genId = require('./utils/gen-id') var querystring = require('querystring') var loaderUtils = require('loader-utils') var normalize = require('./utils/normalize') var tryRequire = require('./utils/try-require') // internal lib loaders var selectorPath = normalize.lib('selector') var styleCompilerPath = normalize.lib('style-compiler/index') var templateCompilerPath = normalize.lib('template-compiler/index') var templatePreprocessorPath = normalize.lib('template-compiler/preprocessor') var componentNormalizerPath = normalize.lib('component-normalizer') // dep loaders var styleLoaderPath = normalize.dep('vue-style-loader') var hotReloadAPIPath = normalize.dep('vue-hot-reload-api') // check whether default js loader exists var hasBabel = !!tryRequire('babel-loader') var hasBuble = !!tryRequire('buble-loader') // for mp js var { compileMP, compileMPScript } = require('./mp-compiler') var { defaultPart } = require('./mp-compiler/util') var rewriterInjectRE = /\b(css(?:-loader)?(?:\?[^!]+)?)(?:!|$)/ var defaultLang = { template: 'html', styles: 'css', script: 'js' } // When extracting parts from the source vue file, we want to apply the // loaders chained before vue-loader, but exclude some loaders that simply // produces side effects such as linting. function getRawRequest (context, excludedPreLoaders) { excludedPreLoaders = excludedPreLoaders || /eslint-loader/ return loaderUtils.getRemainingRequest({ resource: context.resource, loaderIndex: context.loaderIndex, loaders: context.loaders.filter(loader => !excludedPreLoaders.test(loader.path)) }) } module.exports = function (content) { // for mp // 针对 entry 的 main.js 处理 page 和 app 的入口文件和配置等 const mpOptions = loaderUtils.getOptions(this) || {} if (!mpOptions.fileExt) { throw new Error( '[mpvue-loader] need "fileExt" option in file "build/vue-loader.conf.js",init a new project and copy the directory "build/" to this porject, or just check the "fileExt" option' ) } if (mpOptions.checkMPEntry) { return compileMP.call(this, content, mpOptions) } this.cacheable() var isServer = this.target === 'node' var isProduction = this.minimize || process.env.NODE_ENV === 'production' var loaderContext = this var query = loaderUtils.getOptions(this) || {} var options = Object.assign({ esModule: true }, this.options.vue, this.vue, query) // disable esModule in inject mode // because import/export must be top-level if (query.inject) { options.esModule = false } // #824 avoid multiple webpack runs complaining about unknown option Object.defineProperty(this.options, '__vueOptions__', { value: options, enumerable: false, configurable: true }) var rawRequest = getRawRequest(this, options.excludedPreLoaders) var filePath = this.resourcePath var fileName = path.basename(filePath) var context = (this._compiler && this._compiler.context) || this.options.context || process.cwd() var moduleId = 'data-v-' + genId(filePath, context, options.hashKey) var shortFilePath = path.relative(context, filePath).replace(/^(\.\.\/)+/, '') var cssLoaderOptions = '' if (!isProduction && this.sourceMap && options.cssSourceMap !== false) { cssLoaderOptions += '?sourceMap' } if (isProduction) { cssLoaderOptions += (cssLoaderOptions ? '&' : '?') + 'minimize' } var bubleOptions = hasBuble && options.buble ? '?' + JSON.stringify(options.buble) : '' var output = '' var parts = parse(content, fileName, this.sourceMap) // fix #153: 根组件没有 style 模块,不生成页面的样式文件,补齐内容方便加载 vendor if (!parts.styles.length) { parts.styles.push(defaultPart('style')) } // fix #562: 组件没有 script 模块,会阻塞编译 if (!parts.script) { parts.script = defaultPart('script') } var hasScoped = parts.styles.some(function (s) { return s.scoped }) var hasComment = parts.template && parts.template.attrs && parts.template.attrs.comments var templateCompilerOptions = '?' + JSON.stringify({ id: moduleId, hasScoped: hasScoped, hasComment: hasComment, transformToRequire: options.transformToRequire, preserveWhitespace: options.preserveWhitespace, fileExt: options.fileExt || { template: 'wxml', style: 'wxss', script: 'js' }, buble: options.buble, // only pass compilerModules if it's a path string compilerModules: typeof options.compilerModules === 'string' ? options.compilerModules : undefined }) // mp compiler 全局模式下注入 babelrc const babelLoaderOptions = mpOptions.globalBabelrc ? { loader: 'babel-loader', options: { extends: mpOptions.globalBabelrc } } : 'babel-loader' var defaultLoaders = { html: templateCompilerPath + templateCompilerOptions, css: options.extractCSS ? getCSSExtractLoader() : styleLoaderPath + '!' + 'css-loader' + cssLoaderOptions, js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? babelLoaderOptions : '' } // check if there are custom loaders specified via // webpack config, otherwise use defaults var loaders = Object.assign({}, defaultLoaders, options.loaders) var preLoaders = options.preLoaders || {} var postLoaders = options.postLoaders || {} var needsHotReload = ( !isServer && !isProduction && (parts.script || parts.template) ) if (needsHotReload) { output += 'var disposed = false\n' } // add requires for styles var cssModules if (parts.styles.length) { var styleInjectionCode = 'function injectStyle (ssrContext) {\n' if (needsHotReload) { styleInjectionCode += ` if (disposed) return\n` } if (isServer) { styleInjectionCode += `var i\n` } parts.styles.forEach(function (style, i) { // require style var requireString = style.src ? getRequireForImport('styles', style, style.scoped) : getRequire('styles', style, i, style.scoped) var hasStyleLoader = requireString.indexOf('style-loader') > -1 var hasVueStyleLoader = requireString.indexOf('vue-style-loader') > -1 // vue-style-loader exposes inject functions during SSR so they are // always called var invokeStyle = isServer && hasVueStyleLoader ? code => `;(i=${code},i.__inject__&&i.__inject__(ssrContext),i)\n` : code => ` ${code}\n` var moduleName = (style.module === true) ? '$style' : style.module // setCssModule if (moduleName) { if (!cssModules) { cssModules = {} if (needsHotReload) { output += `var cssModules = {}\n` } } if (moduleName in cssModules) { loaderContext.emitError('CSS module name "' + moduleName + '" is not unique!') styleInjectionCode += invokeStyle(requireString) } else { cssModules[moduleName] = true // `(vue-)style-loader` exposes the name-to-hash map directly // `css-loader` exposes it in `.locals` // add `.locals` if the user configured to not use style-loader. if (!hasStyleLoader) { requireString += '.locals' } if (!needsHotReload) { styleInjectionCode += invokeStyle('this["' + moduleName + '"] = ' + requireString) } else { // handle hot reload for CSS modules. // we store the exported locals in an object and proxy to it by // defining getters inside component instances' lifecycle hook. styleInjectionCode += invokeStyle(`cssModules["${moduleName}"] = ${requireString}`) + `Object.defineProperty(this, "${moduleName}", { get: function () { return cssModules["${moduleName}"] }})\n` var requirePath = style.src ? getRequireForImportString('styles', style, style.scoped) : getRequireString('styles', style, i, style.scoped) output += `module.hot && module.hot.accept([${requirePath}], function () {\n` + // 1. check if style has been injected ` var oldLocals = cssModules["${moduleName}"]\n` + ` if (!oldLocals) return\n` + // 2. re-import (side effect: updates the <style>) ` var newLocals = ${requireString}\n` + // 3. compare new and old locals to see if selectors changed ` if (JSON.stringify(newLocals) === JSON.stringify(oldLocals)) return\n` + // 4. locals changed. Update and force re-render. ` cssModules["${moduleName}"] = newLocals\n` + ` require("${hotReloadAPIPath}").rerender("${moduleId}")\n` + `})\n` } } } else { styleInjectionCode += invokeStyle(requireString) } }) styleInjectionCode += '}\n' output += styleInjectionCode } // we require the component normalizer function, and call it like so: // normalizeComponent( // scriptExports, // compiledTemplate, // injectStyles, // scopeId, // moduleIdentifier (server only) // ) output += 'var normalizeComponent = require(' + loaderUtils.stringifyRequest(loaderContext, '!' + componentNormalizerPath) + ')\n' // <script> output += '/* script */\n' var script = parts.script if (script) { // for mp js // 需要解析组件的 components 给 mpml 生成用 script = compileMPScript.call(this, script, mpOptions, moduleId) if (options.esModule) { output += script.src ? getImportForImport('script', script) : getImport('script', script) + '\n' } else { output += 'var __vue_script__ = ' + (script.src ? getRequireForImport('script', script) : getRequire('script', script)) + '\n' } // inject loader interop if (query.inject) { output += '__vue_script__ = __vue_script__(injections)\n' } } else { output += 'var __vue_script__ = null\n' } // <template> output += '/* template */\n' var template = parts.template if (template) { if (options.esModule) { output += (template.src ? getImportForImport('template', template) : getImport('template', template)) + '\n' } else { output += 'var __vue_template__ = ' + (template.src ? getRequireForImport('template', template) : getRequire('template', template)) + '\n' } } else { output += 'var __vue_template__ = null\n' } // style output += '/* styles */\n' output += 'var __vue_styles__ = ' + (parts.styles.length ? 'injectStyle' : 'null') + '\n' // scopeId output += '/* scopeId */\n' output += 'var __vue_scopeId__ = ' + (hasScoped ? JSON.stringify(moduleId) : 'null') + '\n' // moduleIdentifier (server only) output += '/* moduleIdentifier (server only) */\n' output += 'var __vue_module_identifier__ = ' + (isServer ? JSON.stringify(hash(this.request)) : 'null') + '\n' // close normalizeComponent call output += 'var Component = normalizeComponent(\n' + ' __vue_script__,\n' + ' __vue_template__,\n' + ' __vue_styles__,\n' + ' __vue_scopeId__,\n' + ' __vue_module_identifier__\n' + ')\n' // development-only code if (!isProduction) { // add filename in dev output += 'Component.options.__file = ' + JSON.stringify(shortFilePath) + '\n' // check named exports output += 'if (Component.esModule && Object.keys(Component.esModule).some(function (key) {' + 'return key !== "default" && key.substr(0, 2) !== "__"' + '})) {' + 'console.error("named exports are not supported in *.vue files.")' + '}\n' // check functional components used with templates if (template) { output += 'if (Component.options.functional) {' + 'console.error("' + '[vue-loader] ' + fileName + ': functional components are not ' + 'supported with templates, they should use render functions.' + '")}\n' } } // add requires for customBlocks if (parts.customBlocks && parts.customBlocks.length) { var addedPrefix = false parts.customBlocks.forEach(function (customBlock, i) { if (loaders[customBlock.type]) { // require customBlock customBlock.src = customBlock.attrs.src var requireString = customBlock.src ? getRequireForImport(customBlock.type, customBlock) : getRequire(customBlock.type, customBlock, i) if (!addedPrefix) { output += '\n/* customBlocks */\n' addedPrefix = true } output += 'var customBlock = ' + requireString + '\n' + 'if (typeof customBlock === "function") {' + 'customBlock(Component)' + '}\n' } }) output += '\n' } if (!query.inject) { // hot reload if (needsHotReload) { output += '\n/* hot reload */\n' + 'if (module.hot) {(function () {\n' + ' var hotAPI = require("' + hotReloadAPIPath + '")\n' + ' hotAPI.install(require("vue"), false)\n' + ' if (!hotAPI.compatible) return\n' + ' module.hot.accept()\n' + ' if (!module.hot.data) {\n' + // initial insert ' hotAPI.createRecord("' + moduleId + '", Component.options)\n' + ' } else {\n' // update if (cssModules) { output += ' if (module.hot.data.cssModules && Object.keys(module.hot.data.cssModules) !== Object.keys(cssModules)) {\n' + ' delete Component.options._Ctor\n' + ' }\n' } output += ' hotAPI.reload("' + moduleId + '", Component.options)\n' + ' }\n' // dispose output += ' module.hot.dispose(function (data) {\n' + (cssModules ? ' data.cssModules = cssModules\n' : '') + ' disposed = true\n' + ' })\n' output += '})()}\n' } // final export if (options.esModule) { output += '\nexport default Component.exports\n' } else { output += '\nmodule.exports = Component.exports\n' } } else { // inject-loader support output = '\n/* dependency injection */\n' + 'module.exports = function (injections) {\n' + output + '\n' + '\nreturn Component.exports\n}' } // done return output // --- helpers --- function getRequire (type, part, index, scoped) { return 'require(' + getRequireString(type, part, index, scoped) + ')' } function getImport (type, part, index, scoped) { return 'import __vue_' + type + '__ from ' + getRequireString(type, part, index, scoped) } function getRequireString (type, part, index, scoped) { return loaderUtils.stringifyRequest(loaderContext, // disable all configuration loaders '!!' + // get loader string for pre-processors getLoaderString(type, part, index, scoped) + // select the corresponding part from the vue file getSelectorString(type, index || 0) + // the url to the actual vue file, including remaining requests rawRequest ) } function getRequireForImport (type, impt, scoped) { return 'require(' + getRequireForImportString(type, impt, scoped) + ')' } function getImportForImport (type, impt, scoped) { return 'import __vue_' + type + '__ from ' + getRequireForImportString(type, impt, scoped) } function getRequireForImportString (type, impt, scoped) { return loaderUtils.stringifyRequest(loaderContext, '!!' + getLoaderString(type, impt, -1, scoped) + impt.src ) } function addCssModulesToLoader (loader, part, index) { if (!part.module) return loader var option = options.cssModules || {} var DEFAULT_OPTIONS = { modules: true, importLoaders: true } var OPTIONS = { localIdentName: '[hash:base64]' } return loader.replace(/((?:^|!)css(?:-loader)?)(\?[^!]*)?/, function (m, $1, $2) { // $1: !css-loader // $2: ?a=b var query = loaderUtils.parseQuery($2 || '?') Object.assign(query, OPTIONS, option, DEFAULT_OPTIONS) if (index !== -1) { // Note: // Class name is generated according to its filename. // Different <style> tags in the same .vue file may generate same names. // Append `_[index]` to class name to avoid this. query.localIdentName += '_' + index } return $1 + '?' + JSON.stringify(query) }) } function buildCustomBlockLoaderString (attrs) { var noSrcAttrs = Object.assign({}, attrs) delete noSrcAttrs.src var qs = querystring.stringify(noSrcAttrs) return qs ? '?' + qs : qs } // stringify an Array of loader objects function stringifyLoaders (loaders) { return loaders.map(function (obj) { return obj && typeof obj === 'object' && typeof obj.loader === 'string' ? obj.loader + (obj.options ? '?' + JSON.stringify(obj.options) : '') : obj }).join('!') } function getLoaderString (type, part, index, scoped) { var loader = getRawLoaderString(type, part, index, scoped) var lang = getLangString(type, part) if (preLoaders[lang]) { loader = loader + ensureBang(preLoaders[lang]) } if (postLoaders[lang]) { loader = ensureBang(postLoaders[lang]) + loader } return loader } function getLangString (type, part) { if (type === 'script' || type === 'template' || type === 'styles') { return part.lang || defaultLang[type] } else { return type } } function getRawLoaderString (type, part, index, scoped) { var lang = part.lang || defaultLang[type] var styleCompiler = '' if (type === 'styles') { // style compiler that needs to be applied for all styles styleCompiler = styleCompilerPath + '?' + JSON.stringify({ // a marker for vue-style-loader to know that this is an import from a vue file vue: true, id: moduleId, scoped: !!scoped, hasInlineConfig: !!query.postcss }) + '!' // normalize scss/sass if no specific loaders have been provided if (!loaders[lang]) { if (lang === 'sass') { lang = 'sass?indentedSyntax' } else if (lang === 'scss') { lang = 'sass' } } } var loader = options.extractCSS && type === 'styles' ? loaders[lang] || getCSSExtractLoader(lang) : loaders[lang] var injectString = (type === 'script' && query.inject) ? 'inject-loader!' : '' if (loader != null) { if (Array.isArray(loader)) { loader = stringifyLoaders(loader) } else if (typeof loader === 'object') { loader = stringifyLoaders([loader]) } if (type === 'styles') { // add css modules loader = addCssModulesToLoader(loader, part, index) // inject rewriter before css loader for extractTextPlugin use cases if (rewriterInjectRE.test(loader)) { loader = loader.replace(rewriterInjectRE, function (m, $1) { return ensureBang($1) + styleCompiler }) } else { loader = ensureBang(loader) + styleCompiler } } // if user defines custom loaders for html, add template compiler to it if (type === 'template' && loader.indexOf(defaultLoaders.html) < 0) { loader = defaultLoaders.html + '!' + loader } return injectString + ensureBang(loader) } else { // unknown lang, infer the loader to be used switch (type) { case 'template': return defaultLoaders.html + '!' + templatePreprocessorPath + '?engine=' + lang + '!' case 'styles': loader = addCssModulesToLoader(defaultLoaders.css, part, index) return loader + '!' + styleCompiler + ensureBang(ensureLoader(lang)) case 'script': return injectString + ensureBang(ensureLoader(lang)) default: loader = loaders[type] if (Array.isArray(loader)) { loader = stringifyLoaders(loader) } return ensureBang(loader + buildCustomBlockLoaderString(part.attrs)) } } } // sass => sass-loader // sass-loader => sass-loader // sass?indentedSyntax!css => sass-loader?indentedSyntax!css-loader function ensureLoader (lang) { return lang.split('!').map(function (loader) { return loader.replace(/^([\w-]+)(\?.*)?/, function (_, name, query) { return (/-loader$/.test(name) ? name : (name + '-loader')) + (query || '') }) }).join('!') } function getSelectorString (type, index) { return selectorPath + '?type=' + ((type === 'script' || type === 'template' || type === 'styles') ? type : 'customBlocks') + '&index=' + index + '!' } function ensureBang (loader) { if (loader.charAt(loader.length - 1) !== '!') { return loader + '!' } else { return loader } } function getCSSExtractLoader (lang) { var extractor var op = options.extractCSS // extractCSS option is an instance of ExtractTextPlugin if (typeof op.extract === 'function') { extractor = op } else { extractor = tryRequire('extract-text-webpack-plugin') if (!extractor) { throw new Error( '[vue-loader] extractCSS: true requires extract-text-webpack-plugin ' + 'as a peer dependency.' ) } } var langLoader = lang ? ensureBang(ensureLoader(lang)) : '' return extractor.extract({ use: 'css-loader' + cssLoaderOptions + '!' + langLoader, fallback: 'vue-style-loader' }) } }