UNPKG

san-loader

Version:

San single-file loader for Webpack

150 lines (135 loc) 5.23 kB
/** * Copyright (c) Baidu Inc. All rights reserved. * * This source code is licensed under the MIT license. * See LICENSE file in the project root for license information. * * @file plugin.js * @author clark-t * @description inspired by https://github.com/vuejs/vue-loader/blob/master/lib/plugin-webpack4.js */ const qs = require('querystring'); const RuleSet = require('webpack/lib/RuleSet'); const id = 'san-loader-plugin'; const NS = 'san-loader'; class SanLoaderPlugin { apply(compiler) { compiler.hooks.compilation.tap(id, compilation => { const normalModuleLoader = compilation.hooks.normalModuleLoader; normalModuleLoader.tap(id, loaderContext => { loaderContext[NS] = true; }); }); const rawRules = compiler.options.module.rules; const {rules} = new RuleSet(rawRules); let sanRuleIndex = rawRules.findIndex(createMatcher('foo.san')); if (sanRuleIndex < 0) { sanRuleIndex = rawRules.findIndex(createMatcher('foo.san.html')); } const sanRule = rules[sanRuleIndex]; const clonedRules = rules.filter(r => r !== sanRule).map(cloneRule); compiler.options.module.rules = [ ...clonedRules, ...rules ]; } } function createMatcher(fakeFile) { return (rule, i) => { // #1201 we need to skip the `include` check when locating the vue rule const clone = Object.assign({}, rule); delete clone.include; const normalized = RuleSet.normalizeRule(clone, {}, ''); return ( !rule.enforce && normalized.resource && normalized.resource(fakeFile) ); }; } function cloneRule(rule) { const {resource, resourceQuery} = rule; let currentResource; const res = Object.assign({}, rule, { resource: { test: resource => { currentResource = resource; return true; } }, resourceQuery: query => { // 首先,经过san-loader拆分之后的style、template、script会变成: // xxx.san?san=&lang=js&type=script // lang=js/css/less/html...,来自element的lang attribute const parsed = qs.parse(query.slice(1)); // 跳过不是san=的,即不是san-loader拆分的文件 if (parsed.san == null) { return false; } // 跳过san-loader拆分后,没有lang的,lang=js/css/less/html... if (resource && parsed.lang == null) { return false; } // 得到一个假的uri,用于判断之前的rule是否匹配 const fakeResourcePath = `${currentResource}.${parsed.lang}`; // 复制之前的loader rule,规则匹配不上的,例如babel的test/\.js$/,如果匹配不上,则跳过 // js文件,则xxx.js,resource(xxx.js) => true => false,则继续 if (resource && !resource(fakeResourcePath)) { return false; } // 同上,匹配的是babel写了resourceQuery的情况 if (resourceQuery && !resourceQuery(query)) { return false; } if (parsed.type === 'style' && parsed.module !== undefined) { const conf = findMatchedLoader('css-loader', rule, currentResource, resourceQuery); // style 会经过很多规则,检查 css-loader 的那一个 if (conf && (!conf.options || !conf.options.modules)) { throw new Error(`css-loader#module not set, required by ${currentResource}${query}`); } } // 经过过滤,则这个babel的rule,必须是query: san=&lang=js 才会过 // 同时跟babel的rule必须resource/resourceQuery是匹配上的 // 这样可以避免给san-loader处理后的文件单独配置后续loader // 实现共享项目的loader rule return true; } }); if (rule.rules) { res.rules = rule.rules.map(cloneRule); } if (rule.oneOf) { res.oneOf = rule.oneOf.map(cloneRule); } return res; } /** * 找到 oneOf 或 use 中匹配的指定 loader * * @argument loaderName 要找的 loader 名 * @argument rule 当前匹配到的大规则,里面可能有 use 或 oneOf * @argument resource 当前 resource * @argument query 当前 resourceQuery * @return 对应 loader 的配置,如果没有匹配返回 undefined */ function findMatchedLoader(loaderName, rule, resource, query) { if (rule.use) { for (let conf of rule.use) { if (conf.loader === loaderName) { return conf; } } } if (rule.oneOf) { for (let subRule of rule.oneOf) { if ( subRule.resource && subRule.resource(resource) || subRule.resourceQuery && subRule.resourceQuery(query) ) { return findMatchedLoader(loaderName, subRule); } } } } SanLoaderPlugin.NS = NS; module.exports = SanLoaderPlugin;