2023-12-18 13:12:25 +08:00
|
|
|
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')
|
2024-01-16 21:26:16 +08:00
|
|
|
const {
|
|
|
|
attrsToQuery,
|
|
|
|
testWebpack5,
|
|
|
|
genMatchResource
|
|
|
|
} = require('./codegen/utils')
|
2023-12-18 13:12:25 +08:00
|
|
|
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')
|
2024-01-16 21:26:16 +08:00
|
|
|
const { resolveCompiler } = require('./compiler')
|
|
|
|
const { setDescriptor } = require('./descriptorCache')
|
2023-12-18 13:12:25 +08:00
|
|
|
|
|
|
|
let errorEmitted = false
|
|
|
|
|
|
|
|
module.exports = function (source) {
|
|
|
|
const loaderContext = this
|
|
|
|
|
|
|
|
if (!errorEmitted && !loaderContext['thread-loader'] && !loaderContext[NS]) {
|
2024-01-16 21:26:16 +08:00
|
|
|
loaderContext.emitError(
|
|
|
|
new Error(
|
|
|
|
`vue-loader was used without the corresponding plugin. ` +
|
|
|
|
`Make sure to include VueLoaderPlugin in your webpack config.`
|
|
|
|
)
|
|
|
|
)
|
2023-12-18 13:12:25 +08:00
|
|
|
errorEmitted = true
|
|
|
|
}
|
|
|
|
|
2024-01-16 21:26:16 +08:00
|
|
|
const stringifyRequest = (r) => loaderUtils.stringifyRequest(loaderContext, r)
|
2023-12-18 13:12:25 +08:00
|
|
|
|
|
|
|
const {
|
2024-01-16 21:26:16 +08:00
|
|
|
mode,
|
2023-12-18 13:12:25 +08:00
|
|
|
target,
|
|
|
|
request,
|
|
|
|
minimize,
|
|
|
|
sourceMap,
|
|
|
|
rootContext,
|
|
|
|
resourcePath,
|
2024-01-16 21:26:16 +08:00
|
|
|
resourceQuery: _resourceQuery = '',
|
|
|
|
_compiler
|
2023-12-18 13:12:25 +08:00
|
|
|
} = loaderContext
|
2024-01-16 21:26:16 +08:00
|
|
|
const isWebpack5 = testWebpack5(_compiler)
|
|
|
|
const rawQuery = _resourceQuery.slice(1)
|
|
|
|
const resourceQuery = rawQuery ? `&${rawQuery}` : ''
|
2023-12-18 13:12:25 +08:00
|
|
|
const incomingQuery = qs.parse(rawQuery)
|
|
|
|
const options = loaderUtils.getOptions(loaderContext) || {}
|
2024-01-16 21:26:16 +08:00
|
|
|
const enableInlineMatchResource =
|
|
|
|
isWebpack5 && Boolean(options.experimentalInlineMatchResource)
|
2023-12-18 13:12:25 +08:00
|
|
|
const isServer = target === 'node'
|
|
|
|
const isShadow = !!options.shadowMode
|
2024-01-16 21:26:16 +08:00
|
|
|
const isProduction =
|
|
|
|
mode === 'production' ||
|
|
|
|
options.productionMode ||
|
|
|
|
minimize ||
|
|
|
|
process.env.NODE_ENV === 'production'
|
|
|
|
|
2023-12-18 13:12:25 +08:00
|
|
|
const filename = path.basename(resourcePath)
|
|
|
|
const context = rootContext || process.cwd()
|
|
|
|
const sourceRoot = path.dirname(path.relative(context, resourcePath))
|
|
|
|
|
2024-01-16 21:26:16 +08:00
|
|
|
const { compiler, templateCompiler } = resolveCompiler(context, loaderContext)
|
|
|
|
|
|
|
|
const descriptor = compiler.parse({
|
2023-12-18 13:12:25 +08:00
|
|
|
source,
|
2024-01-16 21:26:16 +08:00
|
|
|
compiler: options.compiler || templateCompiler,
|
2023-12-18 13:12:25 +08:00
|
|
|
filename,
|
|
|
|
sourceRoot,
|
|
|
|
needMap: sourceMap
|
|
|
|
})
|
|
|
|
|
2024-01-16 21:26:16 +08:00
|
|
|
// 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
|
|
|
|
)
|
|
|
|
|
2023-12-18 13:12:25 +08:00
|
|
|
// 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,
|
2024-01-16 21:26:16 +08:00
|
|
|
id,
|
|
|
|
options,
|
2023-12-18 13:12:25 +08:00
|
|
|
loaderContext,
|
|
|
|
incomingQuery,
|
|
|
|
!!options.appendExtension
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// feature information
|
2024-01-16 21:26:16 +08:00
|
|
|
const hasScoped = descriptor.styles.some((s) => s.scoped)
|
|
|
|
const hasFunctional =
|
|
|
|
descriptor.template && descriptor.template.attrs.functional
|
|
|
|
const needsHotReload =
|
2023-12-18 13:12:25 +08:00
|
|
|
!isServer &&
|
|
|
|
!isProduction &&
|
2024-01-16 21:26:16 +08:00
|
|
|
(descriptor.script || descriptor.scriptSetup || descriptor.template) &&
|
2023-12-18 13:12:25 +08:00
|
|
|
options.hotReload !== false
|
2024-01-16 21:26:16 +08:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2023-12-18 13:12:25 +08:00
|
|
|
|
|
|
|
// template
|
|
|
|
let templateImport = `var render, staticRenderFns`
|
|
|
|
let templateRequest
|
|
|
|
if (descriptor.template) {
|
|
|
|
const src = descriptor.template.src || resourcePath
|
2024-01-16 21:26:16 +08:00
|
|
|
const externalQuery = descriptor.template.src ? `&external` : ``
|
2023-12-18 13:12:25 +08:00
|
|
|
const idQuery = `&id=${id}`
|
|
|
|
const scopedQuery = hasScoped ? `&scoped=true` : ``
|
|
|
|
const attrsQuery = attrsToQuery(descriptor.template.attrs)
|
2024-01-16 21:26:16 +08:00
|
|
|
// 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}`
|
2023-12-18 13:12:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// styles
|
|
|
|
let stylesCode = ``
|
|
|
|
if (descriptor.styles.length) {
|
|
|
|
stylesCode = genStylesCode(
|
|
|
|
loaderContext,
|
|
|
|
descriptor.styles,
|
|
|
|
id,
|
|
|
|
resourcePath,
|
|
|
|
stringifyRequest,
|
|
|
|
needsHotReload,
|
2024-01-16 21:26:16 +08:00
|
|
|
isServer || isShadow, // needs explicit injection?
|
|
|
|
isProduction,
|
|
|
|
enableInlineMatchResource
|
2023-12-18 13:12:25 +08:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-01-16 21:26:16 +08:00
|
|
|
let code =
|
|
|
|
`
|
2023-12-18 13:12:25 +08:00
|
|
|
${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(
|
2024-01-16 21:26:16 +08:00
|
|
|
loaderContext,
|
2023-12-18 13:12:25 +08:00
|
|
|
descriptor.customBlocks,
|
|
|
|
resourcePath,
|
|
|
|
resourceQuery,
|
2024-01-16 21:26:16 +08:00
|
|
|
stringifyRequest,
|
|
|
|
enableInlineMatchResource
|
2023-12-18 13:12:25 +08:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2024-01-16 21:26:16 +08:00
|
|
|
code += `\ncomponent.options.__file = ${JSON.stringify(
|
|
|
|
rawShortFilePath.replace(/\\/g, '/')
|
|
|
|
)}`
|
2023-12-18 13:12:25 +08:00
|
|
|
} else if (options.exposeFilename) {
|
2024-01-16 21:26:16 +08:00
|
|
|
// Libraries can opt-in to expose their components' filenames in production builds.
|
2023-12-18 13:12:25 +08:00
|
|
|
// 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
|