/** * * @param {*} content 文件信息 */ const { DOMParser, XMLSerializer } = require('@xmldom/xmldom'); const { resolve, relative, join } = require('path'); const { isUrlRequest, urlToRequest } = require('loader-utils'); const fs = require('fs'); const assert = require('assert'); const path = require('path'); const BOOLEAN_ATTRS = [ 'wx:else', 'show-info', 'active', 'controls', 'danmu-btn', 'enable-danmu', 'autoplay', 'disabled', 'show-value', 'checked', 'scroll-x', 'scroll-y', 'auto-focus', 'focus', 'auto-height', 'password', 'indicator-dots', 'report-submit', 'hidden', 'plain', 'loading', 'redirect', 'loop', 'controls', ]; const OPERATORS = { '<': '<', '<e;': '<=', '>': '>', '>e;': '>=', '&': '&', '"': "'", }; // 替换xmldom生成的无值属性 function replaceBooleanAttr(code) { let reg; BOOLEAN_ATTRS.forEach((v) => { reg = new RegExp(`${v}=['"]${v}['"]`, 'ig'); code = code.replace(reg, v); }); return code; } // 替换xmldom生成的运算符转义 function replaceOperatorAttr(code) { let reg; Object.keys(OPERATORS).forEach((v) => { reg = new RegExp(`${v}`, 'ig'); code = code.replace(reg, OPERATORS[v]); }); return code; } function traverse(doc, callback) { callback(doc); if (doc.childNodes) { const { length } = doc.childNodes; for (let i = 0; i < length; i++) { traverse(doc.childNodes.item(i), callback); } } if (doc.attributes) { const { length } = doc.attributes; for (let i = 0; i < length; i++) { traverse(doc.attributes.item(i), callback); } } } const isSrc = (name) => name === 'src'; const isDynamicSrc = (src) => /\{\{/.test(src); const oakMessage = 'oak-message'; const oakDebugPanel = 'oak-debugPanel'; const I18nModuleName = 'i18n'; function getAppJson(context) { const JSON_PATH = require.resolve(`${context}/app.json`); if (!fs.existsSync(JSON_PATH)) { return; } const data = fs.readFileSync(JSON_PATH, 'utf8'); return JSON.parse(data); } ////////// const { parseSync, transformFromAstSync } = require('@babel/core'); const t = require('@babel/types'); const traverseAst = require('@babel/traverse').default; /** * 判断代码段中是否有t() * @param {*} text * @returns */ function codeChunkIncludesT(text) { return /{{(\w|\W)*\W*t\((\w|\W)+\)(\w|\W)*}}/g.test(text) } /** * 改写代码段中的t()部分 * @param {*} text * @param {*} namespace * @param {*} moduleName * @returns */ function transformCode(text, namespace, moduleName) { const codeChunkRegex = /(?:\{\{)(.*?)(?:\}\})/gm; const matches = text.match(codeChunkRegex); if (!matches) { return text; } let text2 = text; while (matches.length) { const codeChunk = matches.shift(); if (codeChunkIncludesT(codeChunk)) { const codeContent = codeChunk.replace(codeChunkRegex, "$1"); const ast = parseSync(codeContent); traverseAst(ast, { enter(path) { if (path.isCallExpression()) { const { node } = path; if (t.isIdentifier(node.callee) && node.callee.name === 't') { const { arguments } = node; // 在t的后面加五个参数(oakLocales, oakLng, oakDefaultLng, oakNamespace, // 增强能力:如果t后面的第二个参数是字符串,说明想指定oakNamespace,在一些抽象模块中会有此需求 if (arguments[1] && t.isStringLiteral(arguments[1])) { arguments.splice( 1, 0, t.identifier('oakLocales'), t.identifier('oakLng'), t.identifier('oakDefaultLng') ); arguments.push( t.stringLiteral(moduleName) ); } else { arguments.push( t.identifier('oakLocales'), t.identifier('oakLng'), t.identifier('oakDefaultLng'), t.stringLiteral(namespace), t.stringLiteral(moduleName) ); } node.callee = t.memberExpression( t.identifier('i18n'), t.identifier('t') ); } } }, }); const { code } = transformFromAstSync(ast); assert(code.endsWith(';')); text2 = text2.replace(codeContent, code.slice(0, code.length - 1).replaceAll('\n', '')); } } return text2; } ////////// const ModuleNameDict = {}; // 根据当前处理的文件路径推导出wxs目录相对应的路径 function parseXmlFile(appRootPath, appRootSrcPath, appSrcPath, filePath) { // 目前所有的pages/components应当都位于appRootSrcPath下 const isSelf = filePath.startsWith(appRootSrcPath); const filePath2 = filePath.replace(/\\/g, '/'); const fileProjectPath = filePath2.replace(/((\w|\W)*)(\/src|\/lib|\/es)(\/pages\/|\/components\/)((\w|\W)*)/g, '$1'); let moduleName = ModuleNameDict[fileProjectPath]; if (!moduleName) { const { name } = require(join(fileProjectPath, 'package.json')); moduleName = ModuleNameDict[fileProjectPath] = name; } const relativePath = filePath2.replace(/(\w|\W)*(\/pages\/|\/components\/)((\w|\W)*)/g, '$3'); assert(relativePath); const ns = `${moduleName}-${filePath.includes('pages') ? 'p' : 'c'}-${relativePath.replace(/\//g, '-')}`; let level = relativePath.split('/').length + 1; // 加上pages的深度,未来根据isSelf还要进一步处理 let wxsRelativePath = ''; while (level-- > 0) { wxsRelativePath += '../'; } wxsRelativePath += 'wxs'; return { wxsRelativePath, ns, moduleName, }; } module.exports = async function (content) { const options = this.getOptions() || {}; //获取配置参数 const { appSrcPath, appRootPath, appRootSrcPath, cacheDirectory = true } = options; // context 本项目路径 // loader的缓存功能 this.cacheable && this.cacheable(cacheDirectory); const callback = this.async(); const { options: webpackLegacyOptions, _module = {}, _compilation = {}, _compiler = {}, resourcePath, } = this; const { output, mode } = _compiler.options; const { path: outputPath } = output; const { context: filePath, target } = webpackLegacyOptions || this; const issuer = _compilation.moduleGraph.getIssuer(this._module); const issuerContext = (issuer && issuer.context) || filePath; const root = resolve(filePath, issuerContext); let source = content; const { wxsRelativePath, ns, moduleName, } = parseXmlFile(appRootPath, appRootSrcPath, appSrcPath, filePath); const i18nWxsFile = `${wxsRelativePath}/${I18nModuleName}.wxs`; // 无条件注入i18n.wxs source = `\n\n` + source; // 注入全局message组件 if (/pages/.test(filePath)) { const appJson = getAppJson(appSrcPath); if ( appJson && appJson.usingComponents && appJson.usingComponents[oakMessage] ) { source = source + `\n <${oakMessage}>`; } if ( mode !== 'production' && appJson && appJson.usingComponents && appJson.usingComponents[oakDebugPanel] ) { source = source + `\n <${oakDebugPanel}>`; } } const doc = new DOMParser({ errorHandler: { warning(x) { if ( x.indexOf('missed value!!') === -1 && x.indexOf('missed quot(")!') === -1 && x.indexOf('unclosed xml attribute') == -1 ) { console.warn(`${filePath}文件出现警告:${x}`); } }, }, }).parseFromString(source, 'text/xml'); const requests = []; traverse(doc, (node) => { if (node.nodeType === node.ATTRIBUTE_NODE) { if (codeChunkIncludesT(node.value)) { const newVal = transformCode(node.value, ns, moduleName); node.value = newVal; } } if (node.nodeType === node.ELEMENT_NODE) { // xml存在src path路径 if (node.hasAttribute('src')) { const value = node.getAttribute('src'); if ( value && !isDynamicSrc(value) && isUrlRequest(value, root) ) { if (i18nWxsFile === value) { // dist目录下生成一个i18n/locales.wxs文件 /* const path = resolve(outputPath, WXS_PATH); if (!fs.existsSync(replaceDoubleSlash(path))) { const wxsContent = `${getWxsCode()}`; this.emitFile(WXS_PATH, wxsContent); } */ } else { const path = resolve(root, value); // const request = urlToRequest(value, root); requests.push(path); } } } // xml存在oakPath路径,如果有oakFullpath,加上不为undefined的判定 if (node.hasAttribute("oakPath")) { const value = node.getAttribute('oakPath'); if (value.includes('oakFullpath')) { // 临时代码,去掉jichuang中原来的三元操作符 if (value.includes('?')) { console.warn(`${filePath},当前oakFullpath的有效性改成注入判定,不需要在代码中使用三元操作符处理为空的情况`); } if (node.parentNode.nodeName === 'block' && node.parentNode.hasAttribute('wx:if') && node.parentNode.getAttribute('wx:if').includes('oakFullpath')) { const InjectedAttr = node.parentNode.getAttribute('oakInjected'); if (!InjectedAttr) { console.warn(`${filePath},当前oakFullpath的有效性改成注入判定,不需要在上层手动进行oakFullpath的判定`); } } else { const wxIfNode = node.ownerDocument.createElement('block'); wxIfNode.setAttribute('wx:if', '{{oakFullpath}}'); wxIfNode.setAttribute('oakInjected', 'true'); node.parentNode.replaceChild(wxIfNode, node); wxIfNode.appendChild(node); } } } } if (node.nodeType === node.TEXT_NODE) { if (codeChunkIncludesT(node.nodeValue)) { const newVal = transformCode(node.nodeValue, ns, moduleName); node.deleteData(0, node.nodeValue.length); node.insertData(0, newVal); } } }); const loadModule = (request) => new Promise((resolve, reject) => { this.addDependency(request); this.loadModule(request, (err, src) => { /* istanbul ignore if */ if (err) { reject(err); } else { resolve(src); } }); }); try { for (const req of requests) { const module = await loadModule(req); } let code = new XMLSerializer().serializeToString(doc); code = replaceBooleanAttr(code); code = replaceOperatorAttr(code); callback(null, code); } catch (err) { callback(err, content); } };