335 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			335 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const { resolveCompiler } = require('./compiler')
 | |
| 
 | |
| const qs = require('querystring')
 | |
| const id = 'vue-loader-plugin'
 | |
| const NS = 'vue-loader'
 | |
| const BasicEffectRulePlugin = require('webpack/lib/rules/BasicEffectRulePlugin')
 | |
| const BasicMatcherRulePlugin = require('webpack/lib/rules/BasicMatcherRulePlugin')
 | |
| const RuleSetCompiler = require('webpack/lib/rules/RuleSetCompiler')
 | |
| const UseEffectRulePlugin = require('webpack/lib/rules/UseEffectRulePlugin')
 | |
| 
 | |
| const objectMatcherRulePlugins = []
 | |
| try {
 | |
|   const ObjectMatcherRulePlugin = require('webpack/lib/rules/ObjectMatcherRulePlugin')
 | |
|   objectMatcherRulePlugins.push(
 | |
|     new ObjectMatcherRulePlugin('assert', 'assertions'),
 | |
|     new ObjectMatcherRulePlugin('descriptionData')
 | |
|   )
 | |
| } catch (e) {
 | |
|   const DescriptionDataMatcherRulePlugin = require('webpack/lib/rules/DescriptionDataMatcherRulePlugin')
 | |
|   objectMatcherRulePlugins.push(new DescriptionDataMatcherRulePlugin())
 | |
| }
 | |
| 
 | |
| const ruleSetCompiler = new RuleSetCompiler([
 | |
|   new BasicMatcherRulePlugin('test', 'resource'),
 | |
|   new BasicMatcherRulePlugin('mimetype'),
 | |
|   new BasicMatcherRulePlugin('dependency'),
 | |
|   new BasicMatcherRulePlugin('include', 'resource'),
 | |
|   new BasicMatcherRulePlugin('exclude', 'resource', true),
 | |
|   new BasicMatcherRulePlugin('conditions'),
 | |
|   new BasicMatcherRulePlugin('resource'),
 | |
|   new BasicMatcherRulePlugin('resourceQuery'),
 | |
|   new BasicMatcherRulePlugin('resourceFragment'),
 | |
|   new BasicMatcherRulePlugin('realResource'),
 | |
|   new BasicMatcherRulePlugin('issuer'),
 | |
|   new BasicMatcherRulePlugin('compiler'),
 | |
|   ...objectMatcherRulePlugins,
 | |
|   new BasicEffectRulePlugin('type'),
 | |
|   new BasicEffectRulePlugin('sideEffects'),
 | |
|   new BasicEffectRulePlugin('parser'),
 | |
|   new BasicEffectRulePlugin('resolve'),
 | |
|   new BasicEffectRulePlugin('generator'),
 | |
|   new UseEffectRulePlugin()
 | |
| ])
 | |
| 
 | |
| class VueLoaderPlugin {
 | |
|   apply(compiler) {
 | |
|     const normalModule = compiler.webpack
 | |
|       ? compiler.webpack.NormalModule
 | |
|       : require('webpack/lib/NormalModule')
 | |
|     // add NS marker so that the loader can detect and report missing plugin
 | |
|     compiler.hooks.compilation.tap(id, (compilation) => {
 | |
|       const normalModuleLoader =
 | |
|         normalModule.getCompilationHooks(compilation).loader
 | |
|       normalModuleLoader.tap(id, (loaderContext) => {
 | |
|         loaderContext[NS] = true
 | |
|       })
 | |
|     })
 | |
| 
 | |
|     const rules = compiler.options.module.rules
 | |
|     let rawVueRule
 | |
|     let vueRules = []
 | |
| 
 | |
|     for (const rawRule of rules) {
 | |
|       // skip rules with 'enforce'. eg. rule for eslint-loader
 | |
|       if (rawRule.enforce) {
 | |
|         continue
 | |
|       }
 | |
|       vueRules = match(rawRule, 'foo.vue')
 | |
|       if (!vueRules.length) {
 | |
|         vueRules = match(rawRule, 'foo.vue.html')
 | |
|       }
 | |
|       if (vueRules.length > 0) {
 | |
|         if (rawRule.oneOf) {
 | |
|           throw new Error(
 | |
|             `[VueLoaderPlugin Error] vue-loader 15 currently does not support vue rules with oneOf.`
 | |
|           )
 | |
|         }
 | |
|         rawVueRule = rawRule
 | |
|         break
 | |
|       }
 | |
|     }
 | |
|     if (!vueRules.length) {
 | |
|       throw new Error(
 | |
|         `[VueLoaderPlugin Error] No matching rule for .vue files found.\n` +
 | |
|           `Make sure there is at least one root-level rule that matches .vue or .vue.html files.`
 | |
|       )
 | |
|     }
 | |
| 
 | |
|     // get the normalized "use" for vue files
 | |
|     const vueUse = vueRules
 | |
|       .filter((rule) => rule.type === 'use')
 | |
|       .map((rule) => rule.value)
 | |
| 
 | |
|     // get vue-loader options
 | |
|     const vueLoaderUseIndex = vueUse.findIndex((u) => {
 | |
|       return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)
 | |
|     })
 | |
| 
 | |
|     if (vueLoaderUseIndex < 0) {
 | |
|       throw new Error(
 | |
|         `[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` +
 | |
|           `Make sure the rule matching .vue files include vue-loader in its use.`
 | |
|       )
 | |
|     }
 | |
| 
 | |
|     // make sure vue-loader options has a known ident so that we can share
 | |
|     // options by reference in the template-loader by using a ref query like
 | |
|     // template-loader??vue-loader-options
 | |
|     const vueLoaderUse = vueUse[vueLoaderUseIndex]
 | |
|     vueLoaderUse.ident = 'vue-loader-options'
 | |
|     vueLoaderUse.options = vueLoaderUse.options || {}
 | |
|     const enableInlineMatchResource =
 | |
|       vueLoaderUse.options.experimentalInlineMatchResource
 | |
| 
 | |
|     // for each user rule (expect the vue rule), create a cloned rule
 | |
|     // that targets the corresponding language blocks in *.vue files.
 | |
|     const refs = new Map()
 | |
|     const clonedRules = rules
 | |
|       .filter((r) => r !== rawVueRule)
 | |
|       .map((rawRule) =>
 | |
|         cloneRule(rawRule, refs, langBlockRuleCheck, langBlockRuleResource)
 | |
|       )
 | |
| 
 | |
|     // fix conflict with config.loader and config.options when using config.use
 | |
|     delete rawVueRule.loader
 | |
|     delete rawVueRule.options
 | |
|     rawVueRule.use = vueUse
 | |
| 
 | |
|     // rule for template compiler
 | |
|     const templateCompilerRule = {
 | |
|       loader: require.resolve('./loaders/templateLoader'),
 | |
|       resourceQuery: (query) => {
 | |
|         if (!query) {
 | |
|           return false
 | |
|         }
 | |
|         const parsed = qs.parse(query.slice(1))
 | |
|         return parsed.vue != null && parsed.type === 'template'
 | |
|       },
 | |
|       options: vueLoaderUse.options
 | |
|     }
 | |
| 
 | |
|     // for each rule that matches plain .js files, also create a clone and
 | |
|     // match it against the compiled template code inside *.vue files, so that
 | |
|     // compiled vue render functions receive the same treatment as user code
 | |
|     // (mostly babel)
 | |
|     const { is27 } = resolveCompiler(compiler.options.context)
 | |
|     let jsRulesForRenderFn = []
 | |
|     if (is27) {
 | |
|       const skipThreadLoader = true
 | |
|       jsRulesForRenderFn = rules
 | |
|         .filter(
 | |
|           (r) =>
 | |
|             r !== rawVueRule &&
 | |
|             (match(r, 'test.js').length > 0 || match(r, 'test.ts').length > 0)
 | |
|         )
 | |
|         .map((rawRule) => cloneRule(rawRule, refs, jsRuleCheck, jsRuleResource, skipThreadLoader))
 | |
|     }
 | |
| 
 | |
|     // global pitcher (responsible for injecting template compiler loader & CSS
 | |
|     // post loader)
 | |
|     const pitcher = {
 | |
|       loader: require.resolve('./loaders/pitcher'),
 | |
|       resourceQuery: (query) => {
 | |
|         if (!query) {
 | |
|           return false
 | |
|         }
 | |
|         const parsed = qs.parse(query.slice(1))
 | |
|         return parsed.vue != null
 | |
|       },
 | |
|       options: vueLoaderUse.options
 | |
|     }
 | |
|     // replace original rules
 | |
|     if (enableInlineMatchResource) {
 | |
|       // Match rules using `vue-loader`
 | |
|       const vueLoaderRules = rules.filter((rule) => {
 | |
|         const matchOnce = (use) => {
 | |
|           let loaderString = ''
 | |
| 
 | |
|           if (!use) {
 | |
|             return loaderString
 | |
|           }
 | |
| 
 | |
|           if (typeof use === 'string') {
 | |
|             loaderString = use
 | |
|           } else if (Array.isArray(use)) {
 | |
|             loaderString = matchOnce(use[0])
 | |
|           } else if (typeof use === 'object' && use.loader) {
 | |
|             loaderString = use.loader
 | |
|           }
 | |
|           return loaderString
 | |
|         }
 | |
| 
 | |
|         const loader = rule.loader || matchOnce(rule.use)
 | |
|         return (
 | |
|           loader === require('../package.json').name ||
 | |
|           loader.startsWith(require.resolve('./index'))
 | |
|         )
 | |
|       })
 | |
| 
 | |
|       compiler.options.module.rules = [
 | |
|         pitcher,
 | |
|         ...rules.filter((rule) => !vueLoaderRules.includes(rule)),
 | |
|         ...(is27 ? [templateCompilerRule] : []),
 | |
|         ...clonedRules,
 | |
|         ...vueLoaderRules
 | |
|       ]
 | |
|     } else {
 | |
|       compiler.options.module.rules = [
 | |
|         pitcher,
 | |
|         ...jsRulesForRenderFn,
 | |
|         ...(is27 ? [templateCompilerRule] : []),
 | |
|         ...clonedRules,
 | |
|         ...rules
 | |
|       ]
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| const matcherCache = new WeakMap()
 | |
| 
 | |
| function match(rule, fakeFile) {
 | |
|   let ruleSet = matcherCache.get(rule)
 | |
|   if (!ruleSet) {
 | |
|     // skip the `include` check when locating the vue rule
 | |
|     const clonedRawRule = { ...rule }
 | |
|     delete clonedRawRule.include
 | |
| 
 | |
|     ruleSet = ruleSetCompiler.compile([clonedRawRule])
 | |
|     matcherCache.set(rule, ruleSet)
 | |
|   }
 | |
| 
 | |
|   return ruleSet.exec({
 | |
|     resource: fakeFile
 | |
|   })
 | |
| }
 | |
| 
 | |
| const langBlockRuleCheck = (query, rule) => {
 | |
|   return (
 | |
|     query.type === 'custom' || !rule.conditions.length || query.lang != null
 | |
|   )
 | |
| }
 | |
| 
 | |
| const langBlockRuleResource = (query, resource) => `${resource}.${query.lang}`
 | |
| 
 | |
| const jsRuleCheck = (query) => {
 | |
|   return query.type === 'template'
 | |
| }
 | |
| 
 | |
| const jsRuleResource = (query, resource) =>
 | |
|   `${resource}.${query.ts ? `ts` : `js`}`
 | |
| 
 | |
| let uid = 0
 | |
| 
 | |
| function cloneRule(rawRule, refs, ruleCheck, ruleResource, skipThreadLoader) {
 | |
|   const compiledRule = ruleSetCompiler.compileRule(
 | |
|     `clonedRuleSet-${++uid}`,
 | |
|     rawRule,
 | |
|     refs
 | |
|   )
 | |
| 
 | |
|   // do not process rule with enforce
 | |
|   if (!rawRule.enforce) {
 | |
|     const ruleUse = compiledRule.effects
 | |
|       .filter((effect) => effect.type === 'use')
 | |
|       .map((effect) => effect.value)
 | |
|     // fix conflict with config.loader and config.options when using config.use
 | |
|     delete rawRule.loader
 | |
|     delete rawRule.options
 | |
| 
 | |
|     // Filter out `thread-loader` from the `use` array.
 | |
|     // Mitigate https://github.com/vuejs/vue/issues/12828
 | |
|     // Note this won't work if the `use` filed is a function
 | |
|     if (skipThreadLoader && Array.isArray(ruleUse)) {
 | |
|       const isThreadLoader = (loader) => loader === 'thread-loader' || /\/node_modules\/thread-loader\//.test(loader)
 | |
|       rawRule.use = ruleUse.filter(useEntry => {
 | |
|         const loader = typeof useEntry === 'string' ? useEntry : useEntry.loader
 | |
|         return !isThreadLoader(loader)
 | |
|       })
 | |
|     } else {
 | |
|       rawRule.use = ruleUse
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   let currentResource
 | |
|   const res = {
 | |
|     ...rawRule,
 | |
|     resource: (resources) => {
 | |
|       currentResource = resources
 | |
|       return true
 | |
|     },
 | |
|     resourceQuery: (query) => {
 | |
|       if (!query) {
 | |
|         return false
 | |
|       }
 | |
| 
 | |
|       const parsed = qs.parse(query.slice(1))
 | |
|       if (parsed.vue == null) {
 | |
|         return false
 | |
|       }
 | |
|       if (!ruleCheck(parsed, compiledRule)) {
 | |
|         return false
 | |
|       }
 | |
|       const fakeResourcePath = ruleResource(parsed, currentResource)
 | |
|       for (const condition of compiledRule.conditions) {
 | |
|         // add support for resourceQuery
 | |
|         const request =
 | |
|           condition.property === 'resourceQuery' ? query : fakeResourcePath
 | |
|         if (condition && !condition.fn(request)) {
 | |
|           return false
 | |
|         }
 | |
|       }
 | |
|       return true
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   delete res.test
 | |
| 
 | |
|   if (rawRule.rules) {
 | |
|     res.rules = rawRule.rules.map((rule) =>
 | |
|       cloneRule(rule, refs, ruleCheck, ruleResource)
 | |
|     )
 | |
|   }
 | |
| 
 | |
|   if (rawRule.oneOf) {
 | |
|     res.oneOf = rawRule.oneOf.map((rule) =>
 | |
|       cloneRule(rule, refs, ruleCheck, ruleResource)
 | |
|     )
 | |
|   }
 | |
| 
 | |
|   return res
 | |
| }
 | |
| 
 | |
| VueLoaderPlugin.NS = NS
 | |
| module.exports = VueLoaderPlugin
 |