|
| 1 | +const qs = require('querystring') |
| 2 | +const id = 'vue-loader-plugin' |
| 3 | +const NS = 'vue-loader' |
| 4 | +const BasicEffectRulePlugin = require('webpack/lib/rules/BasicEffectRulePlugin') |
| 5 | +const BasicMatcherRulePlugin = require('webpack/lib/rules/BasicMatcherRulePlugin') |
| 6 | +const RuleSetCompiler = require('webpack/lib/rules/RuleSetCompiler') |
| 7 | +const UseEffectRulePlugin = require('webpack/lib/rules/UseEffectRulePlugin') |
| 8 | + |
| 9 | +const ruleSetCompiler = new RuleSetCompiler([ |
| 10 | + new BasicMatcherRulePlugin('test', 'resource'), |
| 11 | + new BasicMatcherRulePlugin('include', 'resource'), |
| 12 | + new BasicMatcherRulePlugin('exclude', 'resource', true), |
| 13 | + new BasicMatcherRulePlugin('resource'), |
| 14 | + new BasicMatcherRulePlugin('conditions'), |
| 15 | + new BasicMatcherRulePlugin('resourceQuery'), |
| 16 | + new BasicMatcherRulePlugin('realResource'), |
| 17 | + new BasicMatcherRulePlugin('issuer'), |
| 18 | + new BasicMatcherRulePlugin('compiler'), |
| 19 | + new BasicEffectRulePlugin('type'), |
| 20 | + new BasicEffectRulePlugin('sideEffects'), |
| 21 | + new BasicEffectRulePlugin('parser'), |
| 22 | + new BasicEffectRulePlugin('resolve'), |
| 23 | + new UseEffectRulePlugin() |
| 24 | +]) |
| 25 | + |
| 26 | +class VueLoaderPlugin { |
| 27 | + apply (compiler) { |
| 28 | + // add NS marker so that the loader can detect and report missing plugin |
| 29 | + compiler.hooks.compilation.tap(id, compilation => { |
| 30 | + const normalModuleLoader = require('webpack/lib/NormalModule').getCompilationHooks(compilation).loader |
| 31 | + normalModuleLoader.tap(id, loaderContext => { |
| 32 | + loaderContext[NS] = true |
| 33 | + }) |
| 34 | + }) |
| 35 | + |
| 36 | + const rules = compiler.options.module.rules |
| 37 | + let rawVueRules |
| 38 | + let vueRules = [] |
| 39 | + |
| 40 | + for (const rawRule of rules) { |
| 41 | + // skip the `include` check when locating the vue rule |
| 42 | + const clonedRawRule = Object.assign({}, rawRule) |
| 43 | + delete clonedRawRule.include |
| 44 | + |
| 45 | + const ruleSet = ruleSetCompiler.compile([{ |
| 46 | + rules: [clonedRawRule] |
| 47 | + }]) |
| 48 | + vueRules = ruleSet.exec({ |
| 49 | + resource: 'foo.vue' |
| 50 | + }) |
| 51 | + |
| 52 | + if (!vueRules.length) { |
| 53 | + vueRules = ruleSet.exec({ |
| 54 | + resource: 'foo.vue.html' |
| 55 | + }) |
| 56 | + } |
| 57 | + if (vueRules.length > 0) { |
| 58 | + if (rawRule.oneOf) { |
| 59 | + throw new Error( |
| 60 | + `[VueLoaderPlugin Error] vue-loader 15 currently does not support vue rules with oneOf.` |
| 61 | + ) |
| 62 | + } |
| 63 | + rawVueRules = rawRule |
| 64 | + break |
| 65 | + } |
| 66 | + } |
| 67 | + if (!vueRules.length) { |
| 68 | + throw new Error( |
| 69 | + `[VueLoaderPlugin Error] No matching rule for .vue files found.\n` + |
| 70 | + `Make sure there is at least one root-level rule that matches .vue or .vue.html files.` |
| 71 | + ) |
| 72 | + } |
| 73 | + |
| 74 | + // get the normlized "use" for vue files |
| 75 | + const vueUse = vueRules.filter(rule => rule.type === 'use').map(rule => rule.value) |
| 76 | + |
| 77 | + // get vue-loader options |
| 78 | + const vueLoaderUseIndex = vueUse.findIndex(u => { |
| 79 | + return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader) |
| 80 | + }) |
| 81 | + |
| 82 | + if (vueLoaderUseIndex < 0) { |
| 83 | + throw new Error( |
| 84 | + `[VueLoaderPlugin Error] No matching use for vue-loader is found.\n` + |
| 85 | + `Make sure the rule matching .vue files include vue-loader in its use.` |
| 86 | + ) |
| 87 | + } |
| 88 | + |
| 89 | + // make sure vue-loader options has a known ident so that we can share |
| 90 | + // options by reference in the template-loader by using a ref query like |
| 91 | + // template-loader??vue-loader-options |
| 92 | + const vueLoaderUse = vueUse[vueLoaderUseIndex] |
| 93 | + vueLoaderUse.ident = 'vue-loader-options' |
| 94 | + vueLoaderUse.options = vueLoaderUse.options || {} |
| 95 | + |
| 96 | + // for each user rule (expect the vue rule), create a cloned rule |
| 97 | + // that targets the corresponding language blocks in *.vue files. |
| 98 | + const refs = new Map() |
| 99 | + const clonedRules = rules |
| 100 | + .filter(r => r !== rawVueRules) |
| 101 | + .map((rawRule) => cloneRule(rawRule, refs)) |
| 102 | + |
| 103 | + // fix conflict with config.loader and config.options when using config.use |
| 104 | + delete rawVueRules.loader |
| 105 | + delete rawVueRules.options |
| 106 | + rawVueRules.use = vueUse |
| 107 | + |
| 108 | + // global pitcher (responsible for injecting template compiler loader & CSS |
| 109 | + // post loader) |
| 110 | + const pitcher = { |
| 111 | + loader: require.resolve('./loaders/pitcher'), |
| 112 | + resourceQuery: query => { |
| 113 | + const parsed = qs.parse(query.slice(1)) |
| 114 | + return parsed.vue != null |
| 115 | + }, |
| 116 | + options: { |
| 117 | + cacheDirectory: vueLoaderUse.options.cacheDirectory, |
| 118 | + cacheIdentifier: vueLoaderUse.options.cacheIdentifier |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + // replace original rules |
| 123 | + compiler.options.module.rules = [ |
| 124 | + pitcher, |
| 125 | + ...clonedRules, |
| 126 | + ...rules |
| 127 | + ] |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +function cloneRule (rawRule, refs) { |
| 132 | + const rules = ruleSetCompiler.compileRules('ruleSet', [{ |
| 133 | + rules: [rawRule] |
| 134 | + }], refs) |
| 135 | + let currentResource |
| 136 | + |
| 137 | + const conditions = rules[0].rules |
| 138 | + .map(rule => rule.conditions) |
| 139 | + // shallow flat |
| 140 | + .reduce((prev, next) => prev.concat(next), []) |
| 141 | + |
| 142 | + // do not process rule with enforce |
| 143 | + if (!rawRule.enforce) { |
| 144 | + const ruleUse = rules[0].rules |
| 145 | + .map(rule => rule.effects |
| 146 | + .filter(effect => effect.type === 'use') |
| 147 | + .map(effect => effect.value) |
| 148 | + ) |
| 149 | + // shallow flat |
| 150 | + .reduce((prev, next) => prev.concat(next), []) |
| 151 | + |
| 152 | + // fix conflict with config.loader and config.options when using config.use |
| 153 | + delete rawRule.loader |
| 154 | + delete rawRule.options |
| 155 | + rawRule.use = ruleUse |
| 156 | + } |
| 157 | + |
| 158 | + const res = Object.assign({}, rawRule, { |
| 159 | + resource: resources => { |
| 160 | + currentResource = resources |
| 161 | + return true |
| 162 | + }, |
| 163 | + resourceQuery: query => { |
| 164 | + const parsed = qs.parse(query.slice(1)) |
| 165 | + if (parsed.vue == null) { |
| 166 | + return false |
| 167 | + } |
| 168 | + if (!conditions) { |
| 169 | + return false |
| 170 | + } |
| 171 | + const fakeResourcePath = `${currentResource}.${parsed.lang}` |
| 172 | + for (const condition of conditions) { |
| 173 | + // add support for resourceQuery |
| 174 | + const request = condition.property === 'resourceQuery' ? query : fakeResourcePath |
| 175 | + if (condition && !condition.fn(request)) { |
| 176 | + return false |
| 177 | + } |
| 178 | + } |
| 179 | + return true |
| 180 | + } |
| 181 | + }) |
| 182 | + |
| 183 | + delete res.test |
| 184 | + |
| 185 | + if (rawRule.oneOf) { |
| 186 | + res.oneOf = rawRule.oneOf.map(rule => cloneRule(rule, refs)) |
| 187 | + } |
| 188 | + |
| 189 | + return res |
| 190 | +} |
| 191 | + |
| 192 | +VueLoaderPlugin.NS = NS |
| 193 | +module.exports = VueLoaderPlugin |
0 commit comments