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 } = 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 = '' } = loaderContext const rawQuery = resourceQuery.slice(1) const inheritQuery = `&${rawQuery}` const incomingQuery = qs.parse(rawQuery) const options = loaderUtils.getOptions(loaderContext) || {} 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?.lang || scriptSetup?.lang // isTS = !!(lang && /tsx?/.test(lang)) const src = (script && !scriptSetup && script.src) || resourcePath const attrsQuery = attrsToQuery((scriptSetup || script).attrs, 'js') const query = `?vue&type=script${attrsQuery}${inheritQuery}` const request = stringifyRequest(src + query) scriptImport = `import script from ${request}\n` + `export * from ${request}` // support named exports } // template let templateImport = `var render, staticRenderFns` let templateRequest if (descriptor.template) { const src = descriptor.template.src || resourcePath 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}${inheritQuery}` const request = (templateRequest = stringifyRequest(src + query)) templateImport = `import { render, staticRenderFns } from ${request}` } // styles let stylesCode = `` if (descriptor.styles.length) { stylesCode = genStylesCode( loaderContext, descriptor.styles, id, resourcePath, stringifyRequest, needsHotReload, isServer || isShadow, // needs explicit injection? isProduction ) } 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( descriptor.customBlocks, resourcePath, resourceQuery, stringifyRequest ) } 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