232 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			232 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const path = require('path')
 | 
						|
const hash = require('hash-sum')
 | 
						|
const qs = require('querystring')
 | 
						|
const plugin = require('./plugin')
 | 
						|
const selectBlock = require('./select')
 | 
						|
const loaderUtils = require('loader-utils')
 | 
						|
const {
 | 
						|
  attrsToQuery,
 | 
						|
  testWebpack5,
 | 
						|
  genMatchResource
 | 
						|
} = require('./codegen/utils')
 | 
						|
const genStylesCode = require('./codegen/styleInjection')
 | 
						|
const { genHotReloadCode } = require('./codegen/hotReload')
 | 
						|
const genCustomBlocksCode = require('./codegen/customBlocks')
 | 
						|
const componentNormalizerPath = require.resolve('./runtime/componentNormalizer')
 | 
						|
const { NS } = require('./plugin')
 | 
						|
const { resolveCompiler } = require('./compiler')
 | 
						|
const { setDescriptor } = require('./descriptorCache')
 | 
						|
 | 
						|
let errorEmitted = false
 | 
						|
 | 
						|
module.exports = function (source) {
 | 
						|
  const loaderContext = this
 | 
						|
 | 
						|
  if (!errorEmitted && !loaderContext['thread-loader'] && !loaderContext[NS]) {
 | 
						|
    loaderContext.emitError(
 | 
						|
      new Error(
 | 
						|
        `vue-loader was used without the corresponding plugin. ` +
 | 
						|
          `Make sure to include VueLoaderPlugin in your webpack config.`
 | 
						|
      )
 | 
						|
    )
 | 
						|
    errorEmitted = true
 | 
						|
  }
 | 
						|
 | 
						|
  const stringifyRequest = (r) => loaderUtils.stringifyRequest(loaderContext, r)
 | 
						|
 | 
						|
  const {
 | 
						|
    mode,
 | 
						|
    target,
 | 
						|
    request,
 | 
						|
    minimize,
 | 
						|
    sourceMap,
 | 
						|
    rootContext,
 | 
						|
    resourcePath,
 | 
						|
    resourceQuery: _resourceQuery = '',
 | 
						|
    _compiler
 | 
						|
  } = loaderContext
 | 
						|
  const isWebpack5 = testWebpack5(_compiler)
 | 
						|
  const rawQuery = _resourceQuery.slice(1)
 | 
						|
  const resourceQuery = rawQuery ? `&${rawQuery}` : ''
 | 
						|
  const incomingQuery = qs.parse(rawQuery)
 | 
						|
  const options = loaderUtils.getOptions(loaderContext) || {}
 | 
						|
  const enableInlineMatchResource =
 | 
						|
    isWebpack5 && Boolean(options.experimentalInlineMatchResource)
 | 
						|
  const isServer = target === 'node'
 | 
						|
  const isShadow = !!options.shadowMode
 | 
						|
  const isProduction =
 | 
						|
    mode === 'production' ||
 | 
						|
    options.productionMode ||
 | 
						|
    minimize ||
 | 
						|
    process.env.NODE_ENV === 'production'
 | 
						|
 | 
						|
  const filename = path.basename(resourcePath)
 | 
						|
  const context = rootContext || process.cwd()
 | 
						|
  const sourceRoot = path.dirname(path.relative(context, resourcePath))
 | 
						|
 | 
						|
  const { compiler, templateCompiler } = resolveCompiler(context, loaderContext)
 | 
						|
 | 
						|
  const descriptor = compiler.parse({
 | 
						|
    source,
 | 
						|
    compiler: options.compiler || templateCompiler,
 | 
						|
    filename,
 | 
						|
    sourceRoot,
 | 
						|
    needMap: sourceMap
 | 
						|
  })
 | 
						|
 | 
						|
  // cache descriptor
 | 
						|
  setDescriptor(resourcePath, descriptor)
 | 
						|
 | 
						|
  // module id for scoped CSS & hot-reload
 | 
						|
  const rawShortFilePath = path
 | 
						|
    .relative(context, resourcePath)
 | 
						|
    .replace(/^(\.\.[\/\\])+/, '')
 | 
						|
  const shortFilePath = rawShortFilePath.replace(/\\/g, '/')
 | 
						|
  const id = hash(
 | 
						|
    isProduction
 | 
						|
      ? shortFilePath + '\n' + source.replace(/\r\n/g, '\n')
 | 
						|
      : shortFilePath
 | 
						|
  )
 | 
						|
 | 
						|
  // if the query has a type field, this is a language block request
 | 
						|
  // e.g. foo.vue?type=template&id=xxxxx
 | 
						|
  // and we will return early
 | 
						|
  if (incomingQuery.type) {
 | 
						|
    return selectBlock(
 | 
						|
      descriptor,
 | 
						|
      id,
 | 
						|
      options,
 | 
						|
      loaderContext,
 | 
						|
      incomingQuery,
 | 
						|
      !!options.appendExtension
 | 
						|
    )
 | 
						|
  }
 | 
						|
 | 
						|
  // feature information
 | 
						|
  const hasScoped = descriptor.styles.some((s) => s.scoped)
 | 
						|
  const hasFunctional =
 | 
						|
    descriptor.template && descriptor.template.attrs.functional
 | 
						|
  const needsHotReload =
 | 
						|
    !isServer &&
 | 
						|
    !isProduction &&
 | 
						|
    (descriptor.script || descriptor.scriptSetup || descriptor.template) &&
 | 
						|
    options.hotReload !== false
 | 
						|
 | 
						|
  // script
 | 
						|
  let scriptImport = `var script = {}`
 | 
						|
  // let isTS = false
 | 
						|
  const { script, scriptSetup } = descriptor
 | 
						|
  if (script || scriptSetup) {
 | 
						|
    const lang = (script && script.lang) || (scriptSetup && scriptSetup.lang)
 | 
						|
    // isTS = !!(lang && /tsx?/.test(lang))
 | 
						|
    const externalQuery =
 | 
						|
      script && !scriptSetup && script.src ? `&external` : ``
 | 
						|
    const src = (script && !scriptSetup && script.src) || resourcePath
 | 
						|
    const attrsQuery = attrsToQuery((scriptSetup || script).attrs, 'js')
 | 
						|
    const query = `?vue&type=script${attrsQuery}${resourceQuery}${externalQuery}`
 | 
						|
 | 
						|
    let scriptRequest
 | 
						|
    if (enableInlineMatchResource) {
 | 
						|
      scriptRequest = stringifyRequest(
 | 
						|
        genMatchResource(loaderContext, src, query, lang || 'js')
 | 
						|
      )
 | 
						|
    } else {
 | 
						|
      scriptRequest = stringifyRequest(src + query)
 | 
						|
    }
 | 
						|
    scriptImport =
 | 
						|
      `import script from ${scriptRequest}\n` + `export * from ${scriptRequest}` // support named exports
 | 
						|
  }
 | 
						|
 | 
						|
  // template
 | 
						|
  let templateImport = `var render, staticRenderFns`
 | 
						|
  let templateRequest
 | 
						|
  if (descriptor.template) {
 | 
						|
    const src = descriptor.template.src || resourcePath
 | 
						|
    const externalQuery = descriptor.template.src ? `&external` : ``
 | 
						|
    const idQuery = `&id=${id}`
 | 
						|
    const scopedQuery = hasScoped ? `&scoped=true` : ``
 | 
						|
    const attrsQuery = attrsToQuery(descriptor.template.attrs)
 | 
						|
    // const tsQuery =
 | 
						|
    // options.enableTsInTemplate !== false && isTS ? `&ts=true` : ``
 | 
						|
    const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${resourceQuery}${externalQuery}`
 | 
						|
    if (enableInlineMatchResource) {
 | 
						|
      templateRequest = stringifyRequest(
 | 
						|
        // TypeScript syntax in template expressions is not supported in Vue 2, so the lang is always 'js'
 | 
						|
        genMatchResource(loaderContext, src, query, 'js')
 | 
						|
      )
 | 
						|
    } else {
 | 
						|
      templateRequest = stringifyRequest(src + query)
 | 
						|
    }
 | 
						|
    templateImport = `import { render, staticRenderFns } from ${templateRequest}`
 | 
						|
  }
 | 
						|
 | 
						|
  // styles
 | 
						|
  let stylesCode = ``
 | 
						|
  if (descriptor.styles.length) {
 | 
						|
    stylesCode = genStylesCode(
 | 
						|
      loaderContext,
 | 
						|
      descriptor.styles,
 | 
						|
      id,
 | 
						|
      resourcePath,
 | 
						|
      stringifyRequest,
 | 
						|
      needsHotReload,
 | 
						|
      isServer || isShadow, // needs explicit injection?
 | 
						|
      isProduction,
 | 
						|
      enableInlineMatchResource
 | 
						|
    )
 | 
						|
  }
 | 
						|
 | 
						|
  let code =
 | 
						|
    `
 | 
						|
${templateImport}
 | 
						|
${scriptImport}
 | 
						|
${stylesCode}
 | 
						|
 | 
						|
/* normalize component */
 | 
						|
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
 | 
						|
var component = normalizer(
 | 
						|
  script,
 | 
						|
  render,
 | 
						|
  staticRenderFns,
 | 
						|
  ${hasFunctional ? `true` : `false`},
 | 
						|
  ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
 | 
						|
  ${hasScoped ? JSON.stringify(id) : `null`},
 | 
						|
  ${isServer ? JSON.stringify(hash(request)) : `null`}
 | 
						|
  ${isShadow ? `,true` : ``}
 | 
						|
)
 | 
						|
  `.trim() + `\n`
 | 
						|
 | 
						|
  if (descriptor.customBlocks && descriptor.customBlocks.length) {
 | 
						|
    code += genCustomBlocksCode(
 | 
						|
      loaderContext,
 | 
						|
      descriptor.customBlocks,
 | 
						|
      resourcePath,
 | 
						|
      resourceQuery,
 | 
						|
      stringifyRequest,
 | 
						|
      enableInlineMatchResource
 | 
						|
    )
 | 
						|
  }
 | 
						|
 | 
						|
  if (needsHotReload) {
 | 
						|
    code += `\n` + genHotReloadCode(id, hasFunctional, templateRequest)
 | 
						|
  }
 | 
						|
 | 
						|
  // Expose filename. This is used by the devtools and Vue runtime warnings.
 | 
						|
  if (!isProduction) {
 | 
						|
    // Expose the file's full path in development, so that it can be opened
 | 
						|
    // from the devtools.
 | 
						|
    code += `\ncomponent.options.__file = ${JSON.stringify(
 | 
						|
      rawShortFilePath.replace(/\\/g, '/')
 | 
						|
    )}`
 | 
						|
  } else if (options.exposeFilename) {
 | 
						|
    // Libraries can opt-in to expose their components' filenames in production builds.
 | 
						|
    // For security reasons, only expose the file's basename in production.
 | 
						|
    code += `\ncomponent.options.__file = ${JSON.stringify(filename)}`
 | 
						|
  }
 | 
						|
 | 
						|
  code += `\nexport default component.exports`
 | 
						|
  return code
 | 
						|
}
 | 
						|
 | 
						|
module.exports.VueLoaderPlugin = plugin
 |