|
1 | 1 | import * as webpack from 'webpack'
|
2 |
| -import { TemplateCompiler, CompilerOptions } from '@vue/compiler-sfc' |
| 2 | +import path from 'path' |
| 3 | +import qs from 'querystring' |
| 4 | +import hash from 'hash-sum' |
| 5 | +import loaderUtils from 'loader-utils' |
| 6 | +import { |
| 7 | + parse, |
| 8 | + TemplateCompiler, |
| 9 | + CompilerOptions, |
| 10 | + SFCBlock, |
| 11 | + TemplateCompileOptions |
| 12 | +} from '@vue/compiler-sfc' |
| 13 | +import { selectBlock } from './select' |
| 14 | + |
| 15 | +const VueLoaderPlugin = require('./plugin') |
3 | 16 |
|
4 | 17 | export interface VueLoaderOptions {
|
5 |
| - transformAssetUrls?: { [tag: string]: string | Array<string> } |
| 18 | + transformAssetUrls?: TemplateCompileOptions['transformAssetUrls'] |
6 | 19 | compiler?: TemplateCompiler
|
7 | 20 | compilerOptions?: CompilerOptions
|
8 | 21 | hotReload?: boolean
|
9 | 22 | productionMode?: boolean
|
10 | 23 | cacheDirectory?: string
|
11 | 24 | cacheIdentifier?: string
|
12 | 25 | exposeFilename?: boolean
|
| 26 | + appendExtension?: boolean |
13 | 27 | }
|
14 | 28 |
|
15 |
| -const vueLoader: webpack.loader.Loader = function (source) { |
| 29 | +let errorEmitted = false |
| 30 | + |
| 31 | +const loader: webpack.loader.Loader = function(source) { |
16 | 32 | const loaderContext = this
|
| 33 | + |
| 34 | + // check if plugin is installed |
| 35 | + if ( |
| 36 | + !errorEmitted && |
| 37 | + !(loaderContext as any)['thread-loader'] && |
| 38 | + !(loaderContext as any)[VueLoaderPlugin.NS] |
| 39 | + ) { |
| 40 | + loaderContext.emitError( |
| 41 | + new Error( |
| 42 | + `vue-loader was used without the corresponding plugin. ` + |
| 43 | + `Make sure to include VueLoaderPlugin in your webpack config.` |
| 44 | + ) |
| 45 | + ) |
| 46 | + errorEmitted = true |
| 47 | + } |
| 48 | + |
| 49 | + const stringifyRequest = (r: string) => |
| 50 | + loaderUtils.stringifyRequest(loaderContext, r) |
| 51 | + |
| 52 | + const { |
| 53 | + target, |
| 54 | + minimize, |
| 55 | + sourceMap, |
| 56 | + rootContext, |
| 57 | + resourcePath, |
| 58 | + resourceQuery |
| 59 | + } = loaderContext |
| 60 | + |
| 61 | + const rawQuery = resourceQuery.slice(1) |
| 62 | + const inheritQuery = `&${rawQuery}` |
| 63 | + const incomingQuery = qs.parse(rawQuery) |
| 64 | + const options = (loaderUtils.getOptions(loaderContext) || |
| 65 | + {}) as VueLoaderOptions |
| 66 | + |
| 67 | + const isServer = target === 'node' |
| 68 | + const isProduction = |
| 69 | + options.productionMode || minimize || process.env.NODE_ENV === 'production' |
| 70 | + |
| 71 | + const filename = path.basename(resourcePath) |
| 72 | + const context = rootContext || process.cwd() |
| 73 | + const sourceRoot = path.dirname(path.relative(context, resourcePath)) |
| 74 | + |
| 75 | + const descriptor = parse(String(source), { |
| 76 | + filename, |
| 77 | + sourceMap, |
| 78 | + sourceRoot |
| 79 | + }) |
| 80 | + |
| 81 | + // if the query has a type field, this is a language block request |
| 82 | + // e.g. foo.vue?type=template&id=xxxxx |
| 83 | + // and we will return early |
| 84 | + if (incomingQuery.type) { |
| 85 | + return selectBlock( |
| 86 | + descriptor, |
| 87 | + loaderContext, |
| 88 | + incomingQuery, |
| 89 | + !!options.appendExtension |
| 90 | + ) |
| 91 | + } |
| 92 | + |
| 93 | + // module id for scoped CSS & hot-reload |
| 94 | + const rawShortFilePath = path |
| 95 | + .relative(context, resourcePath) |
| 96 | + .replace(/^(\.\.[\/\\])+/, '') |
| 97 | + const shortFilePath = rawShortFilePath.replace(/\\/g, '/') + resourceQuery |
| 98 | + const id = hash(isProduction ? shortFilePath + '\n' + source : shortFilePath) |
| 99 | + |
| 100 | + // feature information |
| 101 | + const hasScoped = descriptor.styles.some(s => s.scoped) |
| 102 | + const needsHotReload = |
| 103 | + !isServer && |
| 104 | + !isProduction && |
| 105 | + (descriptor.script || descriptor.template) && |
| 106 | + options.hotReload !== false |
| 107 | + |
| 108 | + // template |
| 109 | + let templateImport = `const render = () => {}` |
| 110 | + let templateRequest |
| 111 | + if (descriptor.template) { |
| 112 | + const src = descriptor.template.src || resourcePath |
| 113 | + const idQuery = `&id=${id}` |
| 114 | + const scopedQuery = hasScoped ? `&scoped=true` : `` |
| 115 | + const attrsQuery = attrsToQuery(descriptor.template.attrs) |
| 116 | + const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}` |
| 117 | + const request = (templateRequest = stringifyRequest(src + query)) |
| 118 | + templateImport = `import render from ${request}` |
| 119 | + } |
| 120 | + |
| 121 | + // script |
| 122 | + let scriptImport = `const script = {}` |
| 123 | + if (descriptor.script) { |
| 124 | + const src = descriptor.script.src || resourcePath |
| 125 | + const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js') |
| 126 | + const query = `?vue&type=script${attrsQuery}${inheritQuery}` |
| 127 | + const request = stringifyRequest(src + query) |
| 128 | + scriptImport = |
| 129 | + `import script from ${request}\n` + `export * from ${request}` // support named exports |
| 130 | + } |
| 131 | + |
| 132 | + // styles |
| 133 | + let stylesCode = `` |
| 134 | + if (descriptor.styles.length) { |
| 135 | + // TODO handle style |
| 136 | + } |
| 137 | + |
| 138 | + let code = [ |
| 139 | + templateImport, |
| 140 | + scriptImport, |
| 141 | + stylesCode, |
| 142 | + `script.render = render` |
| 143 | + ].join('\n') |
| 144 | + |
| 145 | + if (descriptor.customBlocks && descriptor.customBlocks.length) { |
| 146 | + // TODO custom blocks |
| 147 | + } |
| 148 | + |
| 149 | + if (needsHotReload) { |
| 150 | + // TODO hot reload |
| 151 | + templateRequest |
| 152 | + } |
| 153 | + |
| 154 | + // Expose filename. This is used by the devtools and Vue runtime warnings. |
| 155 | + if (!isProduction) { |
| 156 | + // Expose the file's full path in development, so that it can be opened |
| 157 | + // from the devtools. |
| 158 | + code += `\nscript.__file = ${JSON.stringify( |
| 159 | + rawShortFilePath.replace(/\\/g, '/') |
| 160 | + )}` |
| 161 | + } else if (options.exposeFilename) { |
| 162 | + // Libraies can opt-in to expose their components' filenames in production builds. |
| 163 | + // For security reasons, only expose the file's basename in production. |
| 164 | + code += `\nscript.__file = ${JSON.stringify(filename)}` |
| 165 | + } |
| 166 | + |
| 167 | + // finalize |
| 168 | + code += `\n\nexport default script` |
| 169 | + return code |
| 170 | +} |
| 171 | + |
| 172 | +// these are built-in query parameters so should be ignored |
| 173 | +// if the user happen to add them as attrs |
| 174 | +const ignoreList = ['id', 'index', 'src', 'type'] |
| 175 | + |
| 176 | +function attrsToQuery(attrs: SFCBlock['attrs'], langFallback?: string): string { |
| 177 | + let query = `` |
| 178 | + for (const name in attrs) { |
| 179 | + const value = attrs[name] |
| 180 | + if (!ignoreList.includes(name)) { |
| 181 | + query += `&${qs.escape(name)}=${value ? qs.escape(String(value)) : ``}` |
| 182 | + } |
| 183 | + } |
| 184 | + if (langFallback && !(`lang` in attrs)) { |
| 185 | + query += `&lang=${langFallback}` |
| 186 | + } |
| 187 | + return query |
17 | 188 | }
|
18 | 189 |
|
19 |
| -export default vueLoader |
| 190 | +;(loader as any).VueLoaderPlugin = VueLoaderPlugin |
| 191 | +module.exports = loader |
0 commit comments