From 043513851747154f9e1f8fd37d6e6c60a8106f51 Mon Sep 17 00:00:00 2001 From: Xc Date: Fri, 18 Aug 2023 12:33:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86=E4=BA=86=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E5=AF=B9xml=E4=B8=AD=E5=AE=9E=E7=8E=B0i18n=E7=9A=84?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=99=A8=E9=83=A8=E5=88=86=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/babel-plugin/oakI18n.js | 2 +- config/loaders/wxml-loader.js | 188 +++++++++++++++++++++------------ config/mp/webpack.config.js | 4 +- package.json | 3 +- plugins/WechatMpPlugin.js | 94 ++++++++++++++++- test.js | 52 +++++++++ 6 files changed, 269 insertions(+), 74 deletions(-) create mode 100644 test.js diff --git a/config/babel-plugin/oakI18n.js b/config/babel-plugin/oakI18n.js index c816875..87e466f 100644 --- a/config/babel-plugin/oakI18n.js +++ b/config/babel-plugin/oakI18n.js @@ -104,7 +104,7 @@ module.exports = (babel) => { return { visitor: { CallExpression(path, state) { - const { cwd, filename } = state; + const { cwd, filename } = state; const res = resolve(cwd, filename).replace(/\\/g, '/'); // this.props.t/this.t/t // 处理策略为给第二个参数中加上'#oakNameSpace, #oakModule两个参数,告知t模块此文件相应的位置,再加以处理寻找 diff --git a/config/loaders/wxml-loader.js b/config/loaders/wxml-loader.js index 5a8d235..5f44bd6 100644 --- a/config/loaders/wxml-loader.js +++ b/config/loaders/wxml-loader.js @@ -3,9 +3,10 @@ * @param {*} content 文件信息 */ const { DOMParser, XMLSerializer } = require('@xmldom/xmldom'); -const { resolve, relative } = require('path'); +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 = [ @@ -96,7 +97,7 @@ const LOCALE_CHANGE_HANDLER_NAME = '$_localeChange'; const CURRENT_LOCALE_DATA = '$_translations'; const DEFAULT_WXS_FILENAME = 'locales.wxs'; -const WXS_PATH = 'i18n' + '/' +DEFAULT_WXS_FILENAME; +const WXS_PATH = 'i18n' + '/' + DEFAULT_WXS_FILENAME; function existsT(str) { if (!str) return false; @@ -136,9 +137,91 @@ function getAppJson(context) { 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 t2 = text.replace(/{{((\w|\W)*)}}/g, '$1'); + const ast = parseSync(t2); + 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, oakModule) + 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(';')); + return `{{${code.substring(0, code.length - 1)}}}`; +} +////////// + +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)(\/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 { context: projectContext, cacheDirectory = true } = options; // context 本项目路径 + const { appSrcPath, appRootPath, appRootSrcPath, cacheDirectory = true } = options; // context 本项目路径 // loader的缓存功能 this.cacheable && this.cacheable(cacheDirectory); const callback = this.async(); @@ -151,29 +234,38 @@ module.exports = async function (content) { } = this; const { output, mode } = _compiler.options; const { path: outputPath } = output; - const { context, target } = webpackLegacyOptions || this; + const { context: filePath, target } = webpackLegacyOptions || this; const issuer = _compilation.moduleGraph.getIssuer(this._module); - const issuerContext = (issuer && issuer.context) || context; - const root = resolve(context, issuerContext); + const issuerContext = (issuer && issuer.context) || filePath; + const root = resolve(filePath, issuerContext); let source = content; - let wxsRelativePath; // locales.wxs相对路径 + + const { + wxsRelativePath, + ns, + moduleName, + } = parseXmlFile(appRootPath, appRootSrcPath, appSrcPath, filePath); + const i18nWxsFile = `${wxsRelativePath}/${I18nModuleName}.wxs`; + + // 无条件注入i18n.wxs + source = `` + source; //判断是否存在i18n的t函数 - if (existsT(source)) { + /* if (existsT(source)) { //判断加载的xml是否为本项目自身的文件 - const isSelf = context.indexOf(projectContext) !== -1; + const isSelf = filePath.indexOf(appSrcPath) !== -1; if (isSelf) { //本项目xml wxsRelativePath = relative( - context, - projectContext + '/' + WXS_PATH + filePath, + appSrcPath + '/' + WXS_PATH ).replace(/\\/g, '/'); } else { //第三方项目的xml - if (oakRegex.test(context)) { - const p = context.replace(oakRegex, ''); + if (oakRegex.test(filePath)) { + const p = filePath.replace(oakRegex, ''); wxsRelativePath = relative( - projectContext + '/' + p, - projectContext + '/' + WXS_PATH + appSrcPath + '/' + p, + appSrcPath + '/' + WXS_PATH ).replace(/\\/g, '/'); } } @@ -183,10 +275,10 @@ module.exports = async function (content) { `` + source; } - } + } */ // 注入全局message组件 - if (/pages/.test(context)) { - const appJson = getAppJson(projectContext); + if (/pages/.test(filePath)) { + const appJson = getAppJson(appSrcPath); if ( appJson && appJson.usingComponents && @@ -220,8 +312,8 @@ module.exports = async function (content) { const requests = []; traverse(doc, (node) => { if (node.nodeType === node.ATTRIBUTE_NODE) { - if (existsT(node.value)) { - const newVal = formatI18nT(node.value, resourcePath); + if (codeChunkIncludesT(node.value)) { + const newVal = transformCode(node.value, ns, moduleName); node.value = newVal; } } @@ -234,13 +326,13 @@ module.exports = async function (content) { !isDynamicSrc(value) && isUrlRequest(value, root) ) { - if (wxsRelativePath === value) { + if (i18nWxsFile === value) { // dist目录下生成一个i18n/locales.wxs文件 - const path = resolve(outputPath, WXS_PATH); + /* 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); @@ -250,8 +342,8 @@ module.exports = async function (content) { } } if (node.nodeType === node.TEXT_NODE) { - if (existsT(node.nodeValue)) { - const newVal = formatI18nT(node.nodeValue, resourcePath); + if (codeChunkIncludesT(node.nodeValue)) { + const newVal = transformCode(node.nodeValue, ns, moduleName); node.deleteData(0, node.nodeValue.length); node.insertData(0, newVal); } @@ -282,48 +374,4 @@ module.exports = async function (content) { } catch (err) { callback(err, content); } -}; - - -function formatI18nT(value, resourcePath) { - // 处理i18n 把t()转成i18n.t() - const p = replaceDoubleSlash(resourcePath).replace( - oakPagesOrComponentsRegex, - '' - ); - const eP = p.substring(0, p.lastIndexOf('/')); - const ns = eP - .split('/') - .filter((ele) => !!ele) - .join('-'); - const val = replaceT(value); // {{i18n.t()}} - const valArr = val.split('}}'); - let newVal = ''; - valArr.forEach((ele, index) => { - if (existsT(ele)) { - const head = ele.substring(0, ele.indexOf('i18n.t(') + 7); - let argsStr = ele.substring(ele.indexOf('i18n.t(') + 7); - argsStr = argsStr.substring(0, argsStr.indexOf(')')); - const end = ele.substring(ele.indexOf(')')); - const arguments = argsStr.split(',').filter((ele2) => !!ele2); - arguments && - arguments.forEach((nodeVal, index) => { - if (index === 0 && nodeVal.indexOf(':') === -1) { - arguments.splice(index, 1, `'${ns}:' + ` + nodeVal); - } - }); - newVal += - head + - arguments.join(',') + - `,${CURRENT_LOCALE_KEY},${CURRENT_LOCALE_DATA} || ''` + - end + - '}}'; - } else if (ele && ele.indexOf('{{') !== -1) { - newVal += ele + '}}'; - } else { - newVal += ele; - } - }); - - return newVal; -} +}; \ No newline at end of file diff --git a/config/mp/webpack.config.js b/config/mp/webpack.config.js index 972db3e..8ec0626 100644 --- a/config/mp/webpack.config.js +++ b/config/mp/webpack.config.js @@ -265,7 +265,9 @@ module.exports = function (webpackEnv) { { loader: 'wxml-loader', options: { - context: paths.appSrc, + appSrcPath: paths.appSrc, + appRootPath: paths.appRootPath, + appRootSrcPath: paths.appRootSrc, cacheDirectory: false, }, }, diff --git a/package.json b/package.json index 7c51104..d202f05 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "lib/index.js", "scripts": { "build": "tsc", - "dev": "tsc --watch" + "dev": "tsc --watch", + "test": "node test.js" }, "bin": { "oak-cli": "lib/index.js" diff --git a/plugins/WechatMpPlugin.js b/plugins/WechatMpPlugin.js index 9fb30a3..048dec0 100644 --- a/plugins/WechatMpPlugin.js +++ b/plugins/WechatMpPlugin.js @@ -11,6 +11,12 @@ const EntryPlugin = require('webpack/lib/EntryPlugin'); const ensurePosix = require('ensure-posix-path'); const requiredPath = require('required-path'); +const { parseSync, transformFromAstSync } = require('@babel/core'); +const t = require('@babel/types'); +const { readFileSync, writeFileSync, existsSync, mkdirSync } = require('fs'); +const assert = require('assert'); +const traverseAst = require('@babel/traverse').default; + const pluginName = 'OakWeChatMpPlugin'; function replaceDoubleSlash(str) { @@ -72,6 +78,17 @@ class OakWeChatMpPlugin { // 输出outpath前 }) ); + + compiler.hooks.afterEmit.tap( + pluginName, + (compilation) => { + const { options, outputOptions } = compilation; + const { context: projectPath } = options; + const { path: outputPath } = outputOptions; + + this.makeI18nWxs(projectPath, outputPath); + } + ); } compilationHooks(compilation) { @@ -278,7 +295,7 @@ class OakWeChatMpPlugin { } } } - } catch (error) {} + } catch (error) { } } // add script entry @@ -675,6 +692,81 @@ class OakWeChatMpPlugin { ...exclude, ]; } + + makeI18nWxs(projectPath, outputPath) { + const i18nWxsFilename = path.join(projectPath, 'node_modules', 'oak-frontend-base', 'lib', 'platforms', 'wechatMp', 'i18n', 'wxs.js'); + const content = readFileSync(i18nWxsFilename, { encoding: 'utf-8' }); + const ast = parseSync(content); + const { program: { body } } = ast; + + traverseAst(ast, { + enter(path) { + if (path.isRegExpLiteral()) { + const { node } = path; + path.replaceWith( + t.callExpression( + t.identifier('getRegExp'), + [ + t.stringLiteral(node.pattern), + t.stringLiteral(node.flags) + ] + ) + ); + } + else if (path.isNewExpression()) { + const { node } = path; + if (t.isIdentifier(node.callee) && node.callee.name === 'RegExp') { + const { arguments: args } = node; + path.replaceWith( + t.callExpression( + t.identifier('getRegExp'), + args + ) + ); + } + } + }, + }); + + /** + * 去掉编译中不必要的expression + */ + ast.program.body = ast.program.body.filter( + ele => !t.isExpressionStatement(ele) + ); + + /** + * 加上module.exports = { t: t }; + */ + ast.program.body.push( + t.expressionStatement( + t.assignmentExpression( + '=', + t.memberExpression( + t.identifier('module'), + t.identifier('exports') + ), + t.objectExpression( + [ + t.objectProperty( + t.identifier('t'), + t.identifier('t') + ) + ] + ) + ) + ) + ); + + const { code } = transformFromAstSync(ast); + + const wxsOutputDir = path.join(outputPath, 'wxs'); + if (!existsSync(wxsOutputDir)) { + mkdirSync(wxsOutputDir); + } + const outputFilename = path.join(wxsOutputDir, 'i18n.wxs'); + writeFileSync(outputFilename, code, { flag: 'w' }); + } } module.exports = OakWeChatMpPlugin; diff --git a/test.js b/test.js new file mode 100644 index 0000000..1183e6d --- /dev/null +++ b/test.js @@ -0,0 +1,52 @@ + +const { parseSync, transformFromAstSync } = require('@babel/core'); +const t = require('@babel/types'); +const assert = require('assert'); +const traverse = require('@babel/traverse').default; + +function codeChunkIncludesT(text) { + return /{{(\w|\W)*\W*t\((\w|\W)+\)(\w|\W)*}}/g.test(text) +} + +function transformCode(text, namespace, moduleName) { + const t2 = text.replace(/{{((\w|\W)*)}}/g, '$1'); + const ast = parseSync(t2); + traverse(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, oakModule) + 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(';')); + return `{{${code.substring(0, code.length - 1)}}}`; +} + +function getRelativePath(filepath) { + const relativePath = filepath.replace(/\\/g, '/').replace(/(\w|\W)*(\/pages\/|\/components\/)((\w|\W)*)/g, '$3'); + return relativePath; +} + +function getProjectPath(filepath) { + const path = filepath.replace(/\\/g, '/').replace(/((\w|\W)*)(\/src|\/lib)(\/pages\/|\/components\/)((\w|\W)*)/g, '$1'); + return path; +} + +// console.log(transformCode('{{abc + t("abd")}}', 'ns', 'module')); +console.log(getProjectPath('D:\\git\\taicang\\src\\pages\\home\\index.xml')); \ No newline at end of file