@lcap/nasl
Version:
NetEase Application Specific Language
634 lines • 25.9 kB
JavaScript
;
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