UNPKG

@lcap/nasl

Version:

NetEase Application Specific Language

634 lines 25.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getGzipData = exports.needFrontendBatch = exports.decodeBundleFileGeneratorConfig = exports.encodeBundleFileGeneratorConfig = exports.getProcessFormDefinitions = exports.genProcessFormCode = exports.setPropertyRecord = exports.getPropertyRecord = exports.getContentType = exports.getValidationRules = exports.transformFrontendsToJson = exports.createGenHashTransformer = exports.getVersionDetail = exports.getAppInfo = exports.getEnvConfig = exports.getPackageInfos = exports.staticResourceSync = exports.getUsedAssets = exports.loadFiles = exports.getFrontendByTypes = exports.replaceAssetUrl = exports.findAllReportIdList = exports.getAuthReport = void 0; // Cross-environment gzip import let gzipSync; // Environment detection and conditional import if (typeof window !== 'undefined' && typeof document !== 'undefined') { // Browser environment - use browser build const { gzipSync: browserGzipSync } = require('fflate/browser'); gzipSync = browserGzipSync; } else if (typeof process !== 'undefined' && process.versions?.node) { // Node.js environment - use Node.js build const { gzipSync: nodeGzipSync } = require('fflate/node'); gzipSync = nodeGzipSync; } else { // Fallback to default import const { gzipSync: defaultGzipSync } = require('fflate'); gzipSync = defaultGzipSync; } const concepts_1 = require("../../concepts"); const utils_1 = require("../../utils"); const types_1 = require("./types"); const nasl_utils_1 = require("@lcap/nasl-utils"); /** * 权限上报数据 */ function getAuthReport(app, frontends) { const result = { roleList: [], resourceList: [], resourceDataList: [], roleResourceMappingList: [], }; result.roleList = app.roles; const getAuth = (target) => { const authDirective = target.bindDirectives.find((item) => item.name === 'auth'); return authDirective?.value; }; const loopViews = (view, isMain, frontend) => { if (view.children) { view.children.forEach((item) => loopViews(item, false, frontend)); } if (view.concept === 'View' && view.elements) { view.elements.forEach((item) => loopViews(item, false, frontend)); } if ((view.concept === 'View' && view.auth) || (view.concept === 'View' && !isMain && view.parentAuth) || (view.concept === 'ViewElement' && getAuth(view))) { result.resourceList.push(view.authPath); result.resourceDataList.push({ value: view.authPath, description: view.authDescription, type: view.concept === 'View' ? 'page' : 'component', clientType: frontend.name, }); if (view.bindRoles) { const _arr = view.bindRoles.map((it) => ({ roleName: it, resourcePath: view.authPath, })); result.roleResourceMappingList.push(..._arr); } } }; frontends.forEach((frontend) => { frontend.views.forEach((view) => loopViews(view, true, frontend)); }); return result; } exports.getAuthReport = getAuthReport; /** * 获取页面中所有 分析报告组件 的reportId */ function findAllReportIdList(app, frontends) { const reportIds = new Set(); frontends.forEach((frontend) => { frontend.views.forEach((view) => loop(view)); }); function loop(root) { if (root.concept === 'ViewElement') { root.bindAttrs.forEach((attr) => { if (attr.name === 'reportId') { reportIds.add(JSON.parse(attr.value ?? '{}').id); } }); } if (root.children && root.children.length) { root.children.forEach((item) => loop(item)); } if (root.concept === 'View' && root.elements && root.elements.length) { root.elements.forEach((item) => loop(item)); } } return Array.from(reportIds).filter(Boolean); } exports.findAllReportIdList = findAllReportIdList; function replaceAssetUrl(files, oldUrlWithoutProtocol, newUrlWithProtocol) { const newUrlWithoutProtocol = newUrlWithProtocol.replace(/^https?:/, ''); if (newUrlWithoutProtocol === oldUrlWithoutProtocol) return files; const reg = new RegExp(`${oldUrlWithoutProtocol}[^?'"\\s]*\\?[^?'"\\s]*(?=("|'))`, 'g'); const reg2 = new RegExp(`(https?:)?${oldUrlWithoutProtocol}`, 'g'); for (const file of files) { // 清除 query 参数 file.content = file.content.replace(reg, (...args) => args[0].split('?')[0]); file.content = file.content.replace(reg2, (whole, protocol) => protocol ? newUrlWithProtocol : newUrlWithoutProtocol); file.name = file.name.replace(reg2, (whole, protocol) => protocol ? newUrlWithProtocol : newUrlWithoutProtocol); } return files; } exports.replaceAssetUrl = replaceAssetUrl; function getFrontendByTypes(frontendTypes = []) { return (frontendTypes?.map((frontendType) => frontendType?.frontends || []) || []).flat(2); } exports.getFrontendByTypes = getFrontendByTypes; /** * 当前应用使用到的资源 * @param axios * @param id * @returns */ async function loadFiles(axios, id) { const { data: res } = await axios.get('/api/v1/user/file', { params: { appId: id, }, }); const assets = res.result?.list || []; return assets; } exports.loadFiles = loadFiles; async function getUsedAssets(axios, id, frontends) { const assets = await loadFiles(axios, id); const urlNameMap = {}; const assetSet = new Set(); for (const { fileUrl, fileKey, name } of assets) { urlNameMap[fileUrl] = fileKey || name; } frontends.forEach((frontend) => { frontend.views.forEach((view) => { view.traverseStrictChildren((ele) => { if (concepts_1.asserts.isBindAttribute(ele) && urlNameMap[ele.value]) { assetSet.add(urlNameMap[ele.value]); } }); }); }); return Array.from(assetSet); } exports.getUsedAssets = getUsedAssets; async function staticResourceSync(axios, assets, id, env, frontends) { const urlNameMap = {}; const assetSet = new Set(); for (const { fileUrl, fileKey, name } of assets) { const key = fileKey ?? name; urlNameMap[fileUrl] = key; // 有些资源在前端是没有协议的前缀的,要兼容此种情况 urlNameMap[fileUrl.replace(/^https?:/, '')] = key; } frontends.forEach((frontend) => { frontend.views.forEach((view) => { view.traverseStrictChildren((ele) => { if (concepts_1.asserts.isBindAttribute(ele) && urlNameMap[ele.value]) { assetSet.add(urlNameMap[ele.value]); } if (concepts_1.asserts.isViewElement(ele) && ele.staticStyle) { const reg = /url\(([^)]+)\)/g; const matches = ele.staticStyle.match(reg); if (matches) { matches.forEach((match) => { const url = match.slice(4, -1); if (urlNameMap[url]) { assetSet.add(urlNameMap[url]); } }); } } }); }); }); const { data: res } = await axios.post('/api/v1/app/env/staticResourceSyncByFileKey', { needSyncFileKeyList: Array.from(assetSet), }, { params: { appId: id, env: env, }, }); if (!res.result?.domain) { throw new Error('资源同步失败!'); } return res.result.domain; } exports.staticResourceSync = staticResourceSync; async function getPackageInfos(axios, app, staticUrl, fullVersion) { const { data: materialCode } = await axios.get(`${staticUrl}/packages/@lcap/mdd-ide@${fullVersion}/dist-mdd-ide/material.config.js`); const window = {}; eval(materialCode); return app.loadPackageInfos(window.LCAP_MATERIALS); } exports.getPackageInfos = getPackageInfos; function getEnvConfig(axios, id) { return axios.get('/api/v1/env/config', { params: { appId: id, }, }).then((data) => { if (!data.data) { throw new Error('获取环境变量失败'); } if (!data.data.success) { throw new Error(`获取环境变量失败,错误消息:${data.data.msg}`); } return data.data.result; }); } exports.getEnvConfig = getEnvConfig; async function getAppInfo(axios, releaseData) { const { config } = releaseData; const [env, { data: { result: appData }, }, { data: { result: tenant }, },] = await Promise.all([ getEnvConfig(axios, releaseData.appId), axios.get('/api/v1/lcpapp/getLcpAppByAppId', { params: { lcpAppId: releaseData.appId, }, }), axios.get('/api/v1/overview/currentTenant', { params: { companyIdentifier: config.tenant, }, }), ]); const isExport = config.mode === types_1.ReleaseMode.ExportCode; return { ...env, tenant: config.tenant, env: env.name, tenantID: env.officialTenantId, isExport, isPreviewFe: config.mode === types_1.ReleaseMode.ExportTemplate, realRelease: config.mode === types_1.ReleaseMode.Release, debug: config.debug, STATIC_URL: isExport ? '' : env.STATIC_URL || 'https://static-vusion.163yun.com', extendedConfig: appData?.extendedConfig, ...tenant, }; } exports.getAppInfo = getAppInfo; async function getVersionDetail(axios, app) { const sampleVersion = app.ideVersion; const { data: versionData } = await axios.get('/api/v1/ide/version/user/detail', { params: { version: sampleVersion, }, }); const fullVersion = versionData.result.version; return { version: sampleVersion, fullVersion: fullVersion, dependencies: versionData.result.dependencies, }; } exports.getVersionDetail = getVersionDetail; /** * 共享的前端JSON转换函数,用于处理u-download组件和类型注解 */ function createGenHashTransformer() { return (source, node) => { // u-download 组件补充 id 为 nodePath if (node?.concept === 'ViewElement' && node?.tag === 'u-download') { source.id = (0, utils_1.genHash)(node.nodePath); } else if (node?.typeAnnotation || node?.__TypeAnnotation) { const typeAnnotation = node?.typeAnnotation || node?.__TypeAnnotation; try { source.typeAnnotation = typeAnnotation?.toJSON(); } catch (e) { source.typeAnnotation = typeAnnotation; } } return source; }; } exports.createGenHashTransformer = createGenHashTransformer; /** * 将前端类型转换为JSON格式 */ function transformFrontendsToJson(frontends) { const transformer = createGenHashTransformer(); return frontends.map((view) => view._toJSON(transformer)); } exports.transformFrontendsToJson = transformFrontendsToJson; // excel导出修改 async function getValidationRules(axios, ideVersion, frontends) { const formData = new FormData(); formData.append('file', new Blob([JSON.stringify(transformFrontendsToJson(frontends))], { type: 'application/json' })); formData.append('appId', frontends[0]?.app?.id); const validations = await axios({ method: 'post', url: `/api/v1/ide/nasl/frontend/call-logic/validations/batch`, headers: { ideVersion: ideVersion, 'Content-Type': 'multipart/form-data', fileDownloadTag: 'true', }, data: formData, }).catch((e) => e); if (!validations.data?.success) { return `获取服务端验证器配置失败:${validations.data?.msg ?? ''}`; } else { return validations?.data?.result ?? {}; } } exports.getValidationRules = getValidationRules; // 根据文件名输出 Content-Type function getContentType(fileName) { if (!fileName) return 'text/plain'; const ext = fileName.split('.').pop().toLowerCase(); switch (ext) { case 'js': return 'application/javascript'; case 'html': return 'text/html'; case 'css': return 'text/css'; case 'json': return 'application/json'; case 'png': return 'image/png'; case 'jpg': case 'jpeg': return 'image/jpeg'; case 'gif': return 'image/gif'; case 'svg': return 'image/svg+xml'; case 'woff': return 'font/woff'; case 'woff2': return 'font/woff2'; case 'ttf': return 'font/ttf'; case 'eot': return 'application/vnd.ms-fontobject'; default: return 'text/plain'; } } exports.getContentType = getContentType; // 获取form中设置了计算类型默认值字段的属性记录,用于补充再toVue产物中,供审批页面中进行字段权限判断使用 const getPropertyRecord = (form) => { // step1: 先找到哪些组件需要记录 const regex = /gen(\w+)_bindAttrs_value_expression_viewMatchExpression/g; const components = [...form.toVue().matchAll(regex)].map((match) => match[1]); if (components?.length === 0) return null; // step2: 找到这些组件的属性记录 let propertyRecord = {}; form.traverseStrictChildren((node) => { if (node?.concept === 'ViewElement' && components.includes(node?.name) && !node?.staticStyle) { const valueAttr = node.getBindAttribute('value'); propertyRecord[node.name] = valueAttr?.value?.split('.')?.[1]; } }); return propertyRecord; }; exports.getPropertyRecord = getPropertyRecord; // 给设置了计算类型默认值的字段,添加属性记录,用于制品侧控制字段权限 const setPropertyRecord = (originForm, currentFormTemplate) => { const records = (0, exports.getPropertyRecord)(originForm); if (records) { currentFormTemplate = Object.entries(records).reduce((template, [key, value]) => template.replace(`:ref="\`${key}\`"`, `:ref="\`${key}\`" :property-record="processDetailFormData.data.${value}"`), currentFormTemplate); } return currentFormTemplate; }; exports.setPropertyRecord = setPropertyRecord; // 移除表单设计器立即创建按钮所在的列节点的模版(todo:定制化逻辑) const filterCreateColumnTemplate = (form, template) => { const createButton = form.children[0].children[0].children.find((column) => column.children[0].getFormControlNode()?.tag?.endsWith('button'))?.children?.[0]?.children?.[0]; if (createButton) { const buttonIndex = template.indexOf(createButton.toVue()), startIndex = template.lastIndexOf('<u-grid-layout-column ', buttonIndex), endIndex = template.indexOf('</u-grid-layout-column>', buttonIndex) + '</u-grid-layout-column>'.length; template = template.substring(0, startIndex) + template.substring(endIndex); } return template; }; /** * 生成流程表单 * @param entity * @param view * @returns * @description 生成流程表单代码,逻辑比较定制化,由于封闭形态下表单修改不容易同步更新审批页流程表单,所以流程页表单在预览生成时补充生成 */ function genProcessFormCode(view) { const processV2 = view.getDestinationProcessV2; let res = ''; if (!processV2?.composedBy) { return res; } const entity = processV2.currentBindEntity, nodes = view.app.getCompose(processV2.composedBy)?.nodes || [], form = nodes.find((node) => node.name === node.composedBy[0]), frontendType = view.frontend.type, processName = processV2.name, tagPrefix = frontendType === 'pc' ? 'u' : 'van'; // res = sliceTagContent(form.toVue(), `${tagPrefix}-form`); // 定义正则表达式 const regex = frontendType === 'pc' ? /<u-form\b[^>]*>([\s\S]*?)<\/u-form>/i : /<van-form\b[^>]*>([\s\S]*?)<\/van-form>/i; // 使用正则表达式匹配内容 const matches = regex.exec(form.toVue({ filterAttr: ['processHidden'] })); // 获取匹配到的内容 res = matches[1]; // 子表单 dataSourceVarName -> relation_data 的映射关系 const relationDataMap = (0, nasl_utils_1.getRelationDataMap)(processV2, 'template'); relationDataMap.forEach((it) => { res = res.replace(new RegExp(`"${it.dataSourceVarName}"`, 'g'), `"${it.relationName}"`); }); res = res.replaceAll(new RegExp(`${entity.name}(?!_)`, 'g'), `processDetailFormData.data`); // TODO: 这样替换可能有问题,需要进一步测试 // 给设置了计算类型默认值的字段,添加属性记录,用于制品侧控制字段权限 // res = setPropertyRecord(form, res); if (frontendType === 'pc') { res = filterCreateColumnTemplate(form, res); } else { res = res.replace(/<van-button[^>]*>.*<\/van-button>/g, ''); } return res; } exports.genProcessFormCode = genProcessFormCode; /** * 权限上报表单定义数据,关联了不同页面的页面的任务节点需要上传不同的表单定义 */ function getProcessFormDefinitions(app, opt) { let res = []; const processV2s = app?.processV2s || []; processV2s.forEach((process) => { const _process = opt?.envTag === 'dev' ? process.getPreviewDefinition : process.getEnabledDefinition; if (!_process) { console.error('流程表单定义获取失败'); return; } _process.elements.forEach((element) => { const attrPC = element?.bindAttrs.find((attr) => attr.name === 'destination-pc'), attrH5 = element?.bindAttrs.find((attr) => attr.name === 'destination-m'); const tempPC = { key: null, content: null, destination: null, }; const tempH5 = { key: null, content: null, destination: null, }; if (attrPC) { tempPC.destination = `${attrPC.destination.viewNamespace}.${attrPC.destination.viewName}`; } if (attrH5) { tempH5.destination = `${attrH5.destination.viewNamespace}.${attrH5.destination.viewName}`; } const patchKey = (attr, type, item) => { if (attr) { const key = `${process.uniqueKey}.${element.name}.${type}`; const view = attr.destination?.viewNode; if (view) { const form = view.getViewProcessForm(); let formDefinition; if (!item.content && form) { formDefinition = form.toVue(); const processPrefix = form.getBindAttribute('processPrefix')?.value; formDefinition = formDefinition.replaceAll(`${processPrefix}.`, 'processDetailFormData.'); formDefinition = formDefinition.replaceAll(`${process.name}.`, 'processDetailFormData.'); formDefinition = formDefinition.replaceAll(`(${process.name} ||`, '(processDetailFormData ||'); formDefinition = formDefinition.replace(/:ref="([^"]+)"/g, (match, p1) => { // ref-name加上方便制品找表单 return `:ref="${p1}" ref-name="${p1.replace(/^`+|`+$/g, '')}" `; }); if (process?.composedBy) { const formInfo = genProcessFormCode(view); // 往form内塞入内容 if (type === 'pc') formDefinition = formDefinition.replace(/<\/u-form>/, `${formInfo}</u-form>`); else formDefinition = formDefinition.replace(/<\/van-form>/, `${formInfo}</van-form>`); } else { // 给设置了计算类型默认值的字段,添加属性记录,用于制品侧控制字段权限 // formDefinition = setPropertyRecord(form, formDefinition); } item.content = formDefinition; } item.key = key; } } }; attrPC && patchKey(attrPC, 'pc', tempPC); attrH5 && patchKey(attrH5, 'm', tempH5); if (tempPC.key && tempPC.content) { res.push(tempPC); } if (tempH5.key && tempH5.content) { res.push(tempH5); } }); }); const resMap = {}; res.forEach((item) => { if (!resMap[item.destination]) { resMap[item.destination] = { keys: [item.key], content: item.content }; } else { resMap[item.destination].keys.push(item.key); } }); const resArr = Object.entries(resMap).map(([destination, { keys, content }]) => { return { keys, content }; }); return resArr; } exports.getProcessFormDefinitions = getProcessFormDefinitions; /** 序列化页面生成器配置 */ function encodeBundleFileGeneratorConfig(config) { const assetsMap = {}; Array.from(config.assetsMap.entries()).forEach(([key, asset]) => { assetsMap[key] = asset; }); // 这里是要过滤掉多余属性 const transformed = { fullVersion: config.fullVersion, tenant: config.tenant, env: config.env, debug: config.debug, nuimsDomain: config.nuimsDomain, STATIC_URL: config.STATIC_URL, USER_STATIC_URL: config.USER_STATIC_URL, extendedConfig: config.extendedConfig, lowcodeDomain: config.lowcodeDomain, envLcpDomain: config.envLcpDomain, envNuimsDomain: config.envNuimsDomain, tenantType: config.tenantType, tenantLevel: config.tenantLevel, appid: config.appid, isExport: config.isExport, sysPrefixPath: config.sysPrefixPath, isPureFeMode: config.isPureFeMode, realRelease: config.realRelease, devDnsAddr: config.devDnsAddr, miniEnable: config.miniEnable, isPreviewFe: config.isPreviewFe, framework: config.framework, previewVersion: config.previewVersion, assets: config.assets, assetsMap: assetsMap, diffNodePaths: config.diffNodePaths, isFull: config.isFull, extensionConfigMap: config.extensionConfigMap, entrancePort: config.entrancePort, }; return JSON.stringify(transformed); } exports.encodeBundleFileGeneratorConfig = encodeBundleFileGeneratorConfig; /** 反序列化页面生成器配置 */ function decodeBundleFileGeneratorConfig(input) { const data = JSON.parse(input); const assetsMap = new Map(); Object.entries(data.assetsMap).forEach(([key, asset]) => { assetsMap.set(key, asset); }); return { ...data, assetsMap, }; } exports.decodeBundleFileGeneratorConfig = decodeBundleFileGeneratorConfig; /** 是否需要启动前端分析 */ function needFrontendBatch(app) { if (app.preferenceMap.onDemandInterfaceGeneration !== 'false' || app.preferenceMap.serverValidationRules !== 'disabled') { return true; } let result = false; for (const frontendType of app.frontendTypes) { for (const frontend of frontendType.frontends) { for (const view of frontend.views) { view.traverseStrictChildrenWithPruning((node) => { // 跳过不需要的节点 if (node.concept === 'Logic' || node.concept === 'BindAttribute') { return true; } if (node.concept === 'ViewElement' && node.tag === 'u-download') { result = true; } // 有结果了,那么跳过全部 if (result) { return true; } }); if (result) { return true; } } } } return false; } exports.needFrontendBatch = needFrontendBatch; /** Base64 转换,将大数组分成小块处理,避免栈溢出 */ function byteArrayToBase64(compressed) { const chunkSize = 8192; // 安全的块大小 let result = ''; for (let i = 0; i < compressed.length; i += chunkSize) { // 截取当前块 const chunk = compressed.slice(i, i + chunkSize); result += String.fromCharCode.apply(null, chunk); } // 对整个结果进行 base64 编码 return btoa(result); } function getGzipData(data) { console.time('GzipData'); const jsonStr = JSON.stringify(data); const uint8Array = new TextEncoder().encode(jsonStr); const compressed = gzipSync(uint8Array, { level: 4, // enough and faster }); const base64 = byteArrayToBase64(compressed); console.timeEnd('GzipData'); return base64; } exports.getGzipData = getGzipData; //# sourceMappingURL=utils.js.map