233 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			233 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const qs = require('querystring')
 | 
						|
const RuleSet = require('webpack/lib/RuleSet')
 | 
						|
const { resolveCompiler } = require('./compiler')
 | 
						|
 | 
						|
const id = 'vue-loader-plugin'
 | 
						|
const NS = 'vue-loader'
 | 
						|
 | 
						|
class VueLoaderPlugin {
 | 
						|
  apply(compiler) {
 | 
						|
    // add NS marker so that the loader can detect and report missing plugin
 | 
						|
    if (compiler.hooks) {
 | 
						|
      // webpack 4
 | 
						|
      compiler.hooks.compilation.tap(id, (compilation) => {
 | 
						|
        const normalModuleLoader = compilation.hooks.normalModuleLoader
 | 
						|
        normalModuleLoader.tap(id, (loaderContext) => {
 | 
						|
          loaderContext[NS] = true
 | 
						|
        })
 | 
						|
      })
 | 
						|
    } else {
 | 
						|
      // webpack < 4
 | 
						|
      compiler.plugin('compilation', (compilation) => {
 | 
						|
        compilation.plugin('normal-module-loader', (loaderContext) => {
 | 
						|
          loaderContext[NS] = true
 | 
						|
        })
 | 
						|
      })
 | 
						|
    }
 | 
						|
 | 
						|
    // use webpack's RuleSet utility to normalize user rules
 | 
						|
    const rawRules = compiler.options.module.rules
 | 
						|
    const { rules } = new RuleSet(rawRules)
 | 
						|
 | 
						|
    // find the rule that applies to vue files
 | 
						|
    let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))
 | 
						|
    if (vueRuleIndex < 0) {
 | 
						|
      vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))
 | 
						|
    }
 | 
						|
    const vueRule = rules[vueRuleIndex]
 | 
						|
 | 
						|
    if (!vueRule) {
 | 
						|
      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.`
 | 
						|
      )
 | 
						|
    }
 | 
						|
 | 
						|
    if (vueRule.oneOf) {
 | 
						|
      throw new Error(
 | 
						|
        `[VueLoaderPlugin Error] vue-loader 15 currently does not support vue rules with oneOf.`
 | 
						|
      )
 | 
						|
    }
 | 
						|
 | 
						|
    // get the normalized "use" for vue files
 | 
						|
    const vueUse = vueRule.use
 | 
						|
    // 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 || {}
 | 
						|
 | 
						|
    // for each user rule (except the vue rule), create a cloned rule
 | 
						|
    // that targets the corresponding language blocks in *.vue files.
 | 
						|
    const clonedRules = rules.filter((r) => r !== vueRule).map(cloneRule)
 | 
						|
 | 
						|
    // rule for template compiler
 | 
						|
    const templateCompilerRule = {
 | 
						|
      loader: require.resolve('./loaders/templateLoader'),
 | 
						|
      resourceQuery: (query) => {
 | 
						|
        const parsed = qs.parse(query.slice(1))
 | 
						|
        return parsed.vue != null && parsed.type === 'template'
 | 
						|
      },
 | 
						|
      options: vueLoaderUse.options
 | 
						|
    }
 | 
						|
 | 
						|
    // for each rule that matches plain .js/.ts 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 matchesJS = createMatcher(`test.js`)
 | 
						|
      // const matchesTS = createMatcher(`test.ts`)
 | 
						|
      jsRulesForRenderFn = rules
 | 
						|
        .filter((r) => r !== vueRule && matchesJS(r))
 | 
						|
        .map(cloneRuleForRenderFn)
 | 
						|
    }
 | 
						|
 | 
						|
    // global pitcher (responsible for injecting template compiler loader & CSS
 | 
						|
    // post loader)
 | 
						|
    const pitcher = {
 | 
						|
      loader: require.resolve('./loaders/pitcher'),
 | 
						|
      resourceQuery: (query) => {
 | 
						|
        const parsed = qs.parse(query.slice(1))
 | 
						|
        return parsed.vue != null
 | 
						|
      },
 | 
						|
      options: {
 | 
						|
        cacheDirectory: vueLoaderUse.options.cacheDirectory,
 | 
						|
        cacheIdentifier: vueLoaderUse.options.cacheIdentifier
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // replace original rules
 | 
						|
    compiler.options.module.rules = [
 | 
						|
      pitcher,
 | 
						|
      ...jsRulesForRenderFn,
 | 
						|
      ...(is27 ? [templateCompilerRule] : []),
 | 
						|
      ...clonedRules,
 | 
						|
      ...rules
 | 
						|
    ]
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function createMatcher(fakeFile) {
 | 
						|
  return (rule, i) => {
 | 
						|
    // #1201 we need to skip the `include` check when locating the vue rule
 | 
						|
    const clone = Object.assign({}, rule)
 | 
						|
    delete clone.include
 | 
						|
    const normalized = RuleSet.normalizeRule(clone, {}, '')
 | 
						|
    return !rule.enforce && normalized.resource && normalized.resource(fakeFile)
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function cloneRule(rule) {
 | 
						|
  const { resource, resourceQuery } = rule
 | 
						|
  // Assuming `test` and `resourceQuery` tests are executed in series and
 | 
						|
  // synchronously (which is true based on RuleSet's implementation), we can
 | 
						|
  // save the current resource being matched from `test` so that we can access
 | 
						|
  // it in `resourceQuery`. This ensures when we use the normalized rule's
 | 
						|
  // resource check, include/exclude are matched correctly.
 | 
						|
  let currentResource
 | 
						|
  const res = Object.assign({}, rule, {
 | 
						|
    resource: {
 | 
						|
      test: (resource) => {
 | 
						|
        currentResource = resource
 | 
						|
        return true
 | 
						|
      }
 | 
						|
    },
 | 
						|
    resourceQuery: (query) => {
 | 
						|
      const parsed = qs.parse(query.slice(1))
 | 
						|
      if (parsed.vue == null) {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
      if (resource && parsed.lang == null) {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
      const fakeResourcePath = `${currentResource}.${parsed.lang}`
 | 
						|
      if (resource && !resource(fakeResourcePath)) {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
      if (resourceQuery && !resourceQuery(query)) {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
      return true
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  if (rule.rules) {
 | 
						|
    res.rules = rule.rules.map(cloneRule)
 | 
						|
  }
 | 
						|
 | 
						|
  if (rule.oneOf) {
 | 
						|
    res.oneOf = rule.oneOf.map(cloneRule)
 | 
						|
  }
 | 
						|
 | 
						|
  return res
 | 
						|
}
 | 
						|
 | 
						|
function cloneRuleForRenderFn(rule) {
 | 
						|
  const resource = rule.resource
 | 
						|
  const resourceQuery = rule.resourceQuery
 | 
						|
  let currentResource
 | 
						|
 | 
						|
  const res = {
 | 
						|
    ...rule,
 | 
						|
    resource: (resource) => {
 | 
						|
      currentResource = resource
 | 
						|
      return true
 | 
						|
    },
 | 
						|
    resourceQuery: (query) => {
 | 
						|
      const parsed = qs.parse(query.slice(1))
 | 
						|
      if (parsed.vue == null || parsed.type !== 'template') {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
      const fakeResourcePath = `${currentResource}.${parsed.ts ? `ts` : `js`}`
 | 
						|
      if (resource && !resource(fakeResourcePath)) {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
      if (resourceQuery && !resourceQuery(query)) {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
      return true
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // 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 (Array.isArray(res.use)) {
 | 
						|
    const isThreadLoader = (loader) => loader === 'thread-loader' || /\/node_modules\/thread-loader\//.test(loader)
 | 
						|
 | 
						|
    res.use = res.use.filter(useEntry => {
 | 
						|
      const loader = typeof useEntry === 'string' ? useEntry : useEntry.loader
 | 
						|
      return !isThreadLoader(loader)
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  if (rule.rules) {
 | 
						|
    res.rules = rule.rules.map(cloneRuleForRenderFn)
 | 
						|
  }
 | 
						|
 | 
						|
  if (rule.oneOf) {
 | 
						|
    res.oneOf = rule.oneOf.map(cloneRuleForRenderFn)
 | 
						|
  }
 | 
						|
 | 
						|
  return res
 | 
						|
}
 | 
						|
 | 
						|
VueLoaderPlugin.NS = NS
 | 
						|
module.exports = VueLoaderPlugin
 |