| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- 'use strict';
- const fs = require('node:fs');
- const path = require('node:path');
- const cp = require('node:child_process');
- const assert = require('node:assert/strict');
- const vm = require('node:vm');
- const { isDeepStrictEqual } = require('node:util');
- const APP = 'APP_ZQ3I7XO2RSHDJ4QDEVNB';
- const FORM = 'FORM-1741B1D56FE04480B0E4F5412FF9AE6ATYGR';
- /** @returns {object} 使用已安装OpenYida的编译、认证、校验和发布传输。 */
- function toolchain() {
- const executable = cp.execFileSync('which', ['openyida'], { encoding: 'utf8' }).trim();
- const root = path.resolve(path.dirname(fs.realpathSync(executable)), '..');
- return {
- transform: require(root + '/lib/core/babel-transform').default,
- guard: require(root + '/lib/core/no-emoji-guard'),
- client: require(root + '/lib/core/yida-client'),
- utils: require(root + '/lib/core/utils'),
- transport: require(root + '/lib/app/publish').sendSaveRequestWithAuth
- };
- }
- /** @param {object} value Schema响应或Schema @returns {object} 完整Schema */
- function schemaContent(value) {
- assert.ok(value && typeof value === 'object' && value.success !== false, 'Schema响应无效');
- const content = value.content === undefined ? value : value.content;
- const schema = typeof content === 'string' ? JSON.parse(content) : content;
- assert.ok(schema && Array.isArray(schema.pages) && schema.actions, '缺少页面或动作配置');
- return schema;
- }
- /** @param {object} schema 完整Schema @returns {{node:object,path:string}} 唯一原生Jsx节点 */
- function jsxNode(schema) {
- const found = [];
- /** @param {object} value 节点 @param {string} pointer JSON路径 @returns {void} */
- function visit(value, pointer) {
- if (!value || typeof value !== 'object') return;
- if (value.componentName === 'Jsx' && value.props && value.props.render) found.push({ node: value, path: pointer });
- for (const [key, child] of Object.entries(value)) visit(child, pointer + '/' + key);
- }
- visit(schema.pages, '/pages');
- assert.equal(found.length, 1, '正式页必须包含唯一的既有Jsx渲染节点');
- return found[0];
- }
- /** @param {string} source 源码 @param {boolean} render 是否为独立渲染 @param {object} tools 工具链 @returns {string} 平台编译码 */
- function compile(source, render, tools) {
- const result = tools.transform(source, {}, render, { RE_VERSION: '7.4.0' });
- if (result.error) throw result.error;
- assert.ok(result.compiled, '平台编译码不能为空');
- new vm.Script(render ? '(' + result.compiled + ')' : result.compiled);
- return result.compiled;
- }
- /** @param {object} baseline 最新基线 @param {string} actions 合并后的JS @param {string} render 合并后的JSX @param {object} tools 工具链 @returns {{schema:object,paths:string[],functions:number}} 限定范围候选 */
- function buildCandidate(baseline, actions, render, tools = toolchain()) {
- const schema = structuredClone(baseline);
- const target = jsxNode(schema);
- const module = { source: actions, compiled: compile(actions, false, tools) };
- const names = [...actions.matchAll(/export\s+(?:async\s+)?function\s+(\w+)\s*\(/g)].map(match => match[1]);
- assert.ok(names.length > 0, '动作模块没有导出函数');
- assert.equal(names.length, new Set(names).size, '动作函数不能重名');
- for (const item of schema.actions.list) assert.ok(names.includes(item.id), '不能移除原动作:' + item.id);
- schema.actions.module = { ...schema.actions.module, ...module };
- const oldNames = new Set(schema.actions.list.map(item => item.id));
- schema.actions.list.push(...names.filter(id => !oldNames.has(id)).map(id => ({ id, title: id })));
- target.node.props.render = { ...target.node.props.render, source: render, compiled: compile(render, true, tools) };
- const paths = ['/actions/module/source', '/actions/module/compiled', '/actions/list',
- target.path + '/props/render/source', target.path + '/props/render/compiled'];
- // prd 只清理当前页面内的连接器描述装饰,不触及URL、参数、鉴权或连接器主资源。
- const root = schema.pages[0].componentsTree[0];
- (root.dataSource.online || []).forEach((dataSource, index) => {
- const connector = dataSource.options && dataSource.options.connector;
- if (!connector || typeof connector.description !== 'string') return;
- const cleaned = connector.description.replace(tools.guard.EMOJI_PATTERN, '');
- if (cleaned === connector.description) return;
- connector.description = cleaned;
- paths.push('/pages/0/componentsTree/0/dataSource/online/' + index + '/options/connector/description');
- });
- tools.guard.assertNoEmojiInValue(schema, { artifact: 'TimeCard双文件候选', code: 'OPENYIDA_FORM_SCHEMA_EMOJI_FORBIDDEN' });
- verifyUnchanged(baseline, schema, paths);
- return { schema, paths, functions: names.length };
- }
- /** @param {object} target 待规范化副本 @param {string} pointer 已验证JSON路径 @returns {void} */
- function omit(target, pointer) {
- const parts = pointer.slice(1).split('/');
- const parent = parts.slice(0, -1).reduce((node, key) => node[key], target);
- delete parent[parts.at(-1)];
- }
- /** @param {object} before 基线 @param {object} after 候选或回读 @param {string[]} paths 允许变化路径 @returns {void} */
- function verifyUnchanged(before, after, paths) {
- const a = structuredClone(before), b = structuredClone(after);
- for (const pointer of [...paths, '/gmtModified']) { omit(a, pointer); omit(b, pointer); }
- assert.deepEqual(b, a, '检测到发布范围外的Schema变化');
- }
- /** @param {object} baseline 候选绑定基线 @param {object} current 保存前线上基线 @returns {void} */
- function assertCurrentBaseline(baseline, current) {
- assert.ok(Number.isFinite(baseline.gmtModified), '缺少服务端版本标识');
- assert.ok(isDeepStrictEqual(baseline, current), '线上页面已变化,必须重新拉取并合并;停止发布');
- }
- /** @param {object} authRef 平台登录引用 @param {object} tools 工具链 @returns {Promise<object>} 只读最新Schema */
- async function fetchSchema(authRef, tools) {
- const result = await tools.utils.requestWithAutoLogin(auth => tools.utils.httpGet(auth.baseUrl,
- '/alibaba/web/' + APP + '/_view/query/formdesign/getFormSchema.json',
- { formUuid: FORM, schemaVersion: 'V5' }), authRef);
- assert.ok(result && result.success !== false && !result.__needLogin && !result.__csrfExpired, '读取线上Schema失败');
- return schemaContent(result);
- }
- /** @param {object} baseline 已备份最新Schema @param {object} candidate 已验证候选 @param {string} output 证据目录 @param {object} tools 工具链 @returns {Promise<void>} 绑定原版本保存并独立回读 */
- async function publish(baseline, candidate, output, tools) {
- const auth = tools.client.createAuthRef();
- const current = await fetchSchema(auth, tools);
- assertCurrentBaseline(baseline, current);
- // fixme 保存始终传候选绑定的原revision,写前及写入时任意并发变化均不能被覆盖。
- fs.writeFileSync(path.join(output, '发布尝试.json'), JSON.stringify({ formUuid: FORM, revision: baseline.gmtModified, attemptedAt: new Date().toISOString() }, null, 2));
- const result = await tools.transport(auth, JSON.stringify(candidate.schema), APP, FORM, baseline.gmtModified);
- fs.writeFileSync(path.join(output, '发布回执.json'), JSON.stringify(result, null, 2));
- assert.ok(result && result.success, '发布未成功;先只读核对回执及线上状态,不自动重试');
- const readback = await fetchSchema(auth, tools);
- fs.writeFileSync(path.join(output, '发布后Schema.json'), JSON.stringify(readback, null, 2));
- verifyUnchanged(candidate.schema, readback, []);
- const receipt = { success: true, formUuid: FORM, version: result.content && result.content.version,
- gmtModified: readback.gmtModified, originalRevision: baseline.gmtModified,
- codeAndConfigurationVerified: true, retainedDataSources: readback.pages[0].componentsTree[0].dataSource.online.length,
- runtimeSmokeVerified: false, url: auth.baseUrl + '/' + APP + '/workbench/' + FORM };
- fs.writeFileSync(path.join(output, '独立回读验收.json'), JSON.stringify(receipt, null, 2));
- console.log(JSON.stringify(receipt));
- }
- /** @returns {Promise<void>} 默认仅生成候选,显式--apply才实际发布指定正式页 */
- async function main() {
- const [baselineFile, outputDir, ...flags] = process.argv.slice(2);
- assert.ok(baselineFile && outputDir && flags.every(flag => flag === '--apply'),
- '用法:node pages/scripts/publish-timecard-main.cjs <最新Schema文件> <证据目录> [--apply]');
- const baseline = schemaContent(JSON.parse(fs.readFileSync(baselineFile, 'utf8')));
- const tools = toolchain();
- const actions = fs.readFileSync(path.join(__dirname, '../src/timecard-main.actions.js'), 'utf8');
- const render = fs.readFileSync(path.join(__dirname, '../src/timecard-main.render.jsx'), 'utf8');
- const candidate = buildCandidate(baseline, actions, render, tools);
- fs.mkdirSync(outputDir, { recursive: true });
- assert.ok(!fs.existsSync(path.join(outputDir, '发布尝试.json')), '证据目录已存在发布尝试,请先核对,不能盲目重试');
- fs.writeFileSync(path.join(outputDir, '候选Schema.json'), JSON.stringify(candidate.schema, null, 2));
- fs.writeFileSync(path.join(outputDir, '候选校验.json'), JSON.stringify({ functions: candidate.functions, allowedPaths: candidate.paths, baselineRevision: baseline.gmtModified }, null, 2));
- if (flags.includes('--apply')) await publish(baseline, candidate, outputDir, tools);
- else console.log(JSON.stringify({ prepared: true, functions: candidate.functions, changedPaths: candidate.paths.length, published: false }));
- }
- module.exports = { buildCandidate, assertCurrentBaseline, verifyUnchanged, schemaContent };
- if (require.main === module) main().catch(error => { console.error(error.message); process.exitCode = 1; });
|