import path from 'path'; import md5 from 'md5'; import { noop, includes, get, defaults } from 'lodash'; import request from '../../utils/request'; import { sleep, tmpdir, compareVersion } from '../../utils/util'; import packAmr from '../../utils/packAmr'; import { readFile } from '../../utils/fs.promise'; import local from '../../utils/local'; import { getProjectConfig, ProjectConfig } from '@miniu/utils'; import miniExperience from './experience'; import { SubApplicationType } from './app.list'; import { getUploadVersion, TYPES_BUNDLEID, MiniUploadVersion, autoAddVersion } from './util'; export interface MiniUploadOptions extends MiniUploadVersion { /** * 本地项目地址 */ project: string; /** * 源码压缩包地址 */ packPath?: string; /** * 上传包的版本,必须大于线上版本 */ packageVersion?: string; /** * 上传成功后,自动设置为体验版本 * 该功能只针对小程序主账号生效 * @default false */ experience?: boolean; /** * 预览流程回调 */ onProgressUpdate?(info: { /** * LOCAL_PACKAGE: 本地打包成功 * UPLOAD_SUCCESS: 上传代码成功 * BUILDING: 正在构建 * BUILD_SUCCESS: 云端构建成功 * EXPERIENCE_FAIL: 体验版本设置失败 */ status: 'LOCAL_PACKAGE' | 'UPLOAD_SUCCESS' | 'BUILDING' | 'BUILD_SUCCESS' | 'EXPERIENCE_FAIL'; data: any; }): void; } interface UploadPackageInfo { /** * 上传包名字 */ name: string; /** * 类型,整包,主包,分包 */ type: 'FULL' | 'MAIN' | 'SUB' | 'SOURCE_MAP'; // TS MODIFY /** * 上传包尺寸,单位KB */ size: string; } export interface MiniUploadResult { /** * 编译后的代码包地址 */ packages: UploadPackageInfo[]; /** * 上传包的版本 */ packageVersion: string; /** * 体验二维码在线地址 */ qrCodeUrl?: string; /** * 版本管理在线地址 */ devManageUrl: string; /** * 程序类型 */ subApplicationType: SubApplicationType; } function parseNebulaInfo(nebulaInfo) { const result = JSON.parse(decodeURIComponent(nebulaInfo)); if (result.detail) { result.detail = JSON.parse(result.detail); } return result; } function getAdaptorNameByType(clientType: keyof typeof TYPES_BUNDLEID): string { if (clientType === 'amap') { return 'gaode'; } return clientType; } // 通过日志,能解析出包信息 function getPackagesByLog(log): UploadPackageInfo[] { if (!log) return []; const logInfo = log.split('\n').filter((item) => { return item.trim(); }); // TS MODIFY const result: UploadPackageInfo[] = []; for (const item of logInfo) { const packageNameMatch = item.match(/dist[\w-]*.tar/); const packageSizeMatch = item.match(/(\d+)B/); if (packageNameMatch && packageSizeMatch) { let type: 'FULL' | 'MAIN' | 'SUB' | 'SOURCE_MAP' = 'SUB'; const packageName = packageNameMatch[0]; if (packageName === 'dist.tar') { type = 'FULL'; } else if (packageName === 'dist-sub-main.tar') { type = 'MAIN'; } else if (includes(packageName, '-map')) { type = 'SOURCE_MAP'; } // 单位换算成KB const size = parseInt(packageSizeMatch[1], 10) / 1024; result.push({ name: packageName, type, size: `${size}KB`, }); } } return result; } // 最大轮训次数, 420次,21分钟 const MAX_LOOP = 420; // 容错次数 const MAX_ERROR_NUM = 5; async function queryCloudBuildResult(data, onProgressUpdate) { let count = 1; let errorCount = 0; // 当前报错次数 let cachedLog = ''; // 每3秒轮询一次 while (count < MAX_LOOP) { await sleep(3000); count += 1; // 开始轮询 const pullingResult = await request<{ nebulaInfo: string; versionCreated: string; packageUrl: string; version: string; needRotation: string; }>({ method: 'POST', host: 'ide', path: '/cli/miniapp/pullingUploadCode.json', needSign: true, data, }); const pullingNebulaInfo = parseNebulaInfo(pullingResult.nebulaInfo); const rawLog: string = get(pullingNebulaInfo, 'detail.log', ''); const inc = rawLog.replace(cachedLog, ''); cachedLog = rawLog; if (inc) { onProgressUpdate({ data: inc, status: 'BUILDING', }); } // 构建成功 if (pullingResult.versionCreated === 'true') { return pullingNebulaInfo; } // 云构建失败 if (pullingResult.needRotation === 'false') { if (errorCount < MAX_ERROR_NUM) { errorCount += 1; continue; } throw new Error('小程序上传失败,请查看日志排查'); } } throw new Error('云构建超时,请联系支付宝小程序技术人员排查'); } function getOldVersion(appId, clientType) { const uploadSuccessKey = `upload_${clientType}_${appId}`; const oldVersion = local.getItem(uploadSuccessKey); return oldVersion || ''; } function setOldVersion(appId, clientType, version) { const uploadSuccessKey = `upload_${clientType}_${appId}`; return local.setItem(uploadSuccessKey, version); } function getDevManageUrl( appId: string, clientType: keyof typeof TYPES_BUNDLEID, subApplicationType: SubApplicationType ): string { const bundleId = TYPES_BUNDLEID[clientType] || TYPES_BUNDLEID.alipay; let linkUrl = 'https://open.alipay.com/mini/dev/sub/dev-manage'; // 插件地址 if (subApplicationType === 'TINYAPP_PLUGIN') { linkUrl = 'https://open.alipay.com/mini/isv/plugin/version'; } return `${linkUrl}?bundleId=${bundleId}&appId=${appId}`; } async function uploadMiniCode(options, projectConfig: ProjectConfig) { const { appId, clientType, onProgressUpdate, packageVersion, packPath } = options; let destPackPath = packPath; if (!destPackPath) { const destDirPath = await tmpdir(); destPackPath = await packAmr({ cwd: projectConfig.projectPath, destDir: destDirPath, miniProjectConfig: projectConfig.miniProjectConfig, }); onProgressUpdate({ status: 'LOCAL_PACKAGE', data: destPackPath, }); } const packBuffer = await readFile(destPackPath); const packageMD5 = md5(packBuffer); const buildParams = { appId, extendInfo: { launchParams: { enableTabBar: 'NO', enableJSC: 'YES', page: projectConfig.appJsonConfig.mainUrl, enableKeepAlive: 'YES', enableWK: 'YES', }, }, mainUrl: `/index.html#${projectConfig.appJsonConfig.mainUrl}`, packageMD5, packageName: path.basename(destPackPath), appType: 'tinyApp', extraInfo: { bundleId: TYPES_BUNDLEID[clientType], tinycliVersion: '6.2.3', tinycliName: 'tiny-cli', adaptorName: getAdaptorNameByType(clientType), }, packageVersion, pluginList: projectConfig.appJsonConfig.pluginList, clientType, clientId: TYPES_BUNDLEID[clientType], oldVersion: getOldVersion(appId, clientType), }; const buildResult = await request<{ buildStatus: string; nebulaInfo: string; needRotation: string; }>({ method: 'POST', host: 'ide', path: '/cli/miniapp/uploadCode.json', needSign: true, beforeSend(req) { req.attach('packageFile', destPackPath); }, data: buildParams, }); const nebulaInfo = parseNebulaInfo(buildResult.nebulaInfo); onProgressUpdate({ status: 'UPLOAD_SUCCESS', data: nebulaInfo, }); return nebulaInfo; } /** * 上传小程序 */ async function miniUpload(options: MiniUploadOptions): Promise { const params = defaults(options, { clientType: 'alipay', onProgressUpdate: noop, }); const { appId, clientType, onProgressUpdate } = params; if (!TYPES_BUNDLEID[clientType]) { throw new Error(`现在支持的端有: ${Object.keys(TYPES_BUNDLEID).join()}`); } const uploadLastVersion = await getUploadVersion({ appId, clientType, }); let packageVersion = params.packageVersion; // 上传版本检测 if (packageVersion) { if (!/^\d+\.\d+\.\d+$/.test(packageVersion)) { throw new Error('版本号必须满足 x.y.z 格式, 且均为数字'); } if (compareVersion(packageVersion, '<=', uploadLastVersion)) { throw new Error(`本次上传版本必须大于当前开发最新版本${uploadLastVersion}`); } } else { packageVersion = autoAddVersion(uploadLastVersion); params.packageVersion = packageVersion; } const projectConfig = await getProjectConfig(params.project); const nebulaInfo = await uploadMiniCode(params, projectConfig); const pullingNebulaInfo = await queryCloudBuildResult( { appId, packageId: nebulaInfo.packageId, packageVersion: nebulaInfo.version, clientType: params.clientType, clientId: TYPES_BUNDLEID[params.clientType], }, onProgressUpdate ); onProgressUpdate({ status: 'BUILD_SUCCESS', }); const result = { packages: getPackagesByLog(pullingNebulaInfo.detail.log), packageVersion: pullingNebulaInfo.version, qrCodeUrl: '', devManageUrl: getDevManageUrl(appId, clientType, projectConfig.subApplicationType), subApplicationType: projectConfig.subApplicationType, expPackageVersion: '', }; // 设置成体验版本 if (options.experience) { try { const miniExperienceResult = await miniExperience({ appId, packageVersion: result.packageVersion, clientType, oldVersion: getOldVersion(appId, clientType), }); result.expPackageVersion = miniExperienceResult.expPackageVersion; result.qrCodeUrl = miniExperienceResult.qrCodeUrl; } catch (e) { onProgressUpdate({ status: 'EXPERIENCE_FAIL', data: '设置小程序体验版本失败,只有小程序的主账号才有此权限', }); throw e; } } // 记录上传成功后的版本 setOldVersion(appId, clientType, pullingNebulaInfo.version); return result; } export default miniUpload;