Files
gongxue-base/scripts/build-weapp-student.js
2026-07-12 19:26:57 +08:00

174 lines
6.3 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const scriptPath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(scriptPath), '..');
const taroRoot = path.join(repoRoot, 'apps', 'taro');
const distProjectConfigPath = path.join(taroRoot, 'dist', 'weapp-student', 'project.config.json');
const placeholderAppIds = new Set([
'wx0000000000000000',
'wx0123456789abcdef',
'wx1234567890abcdef',
'wxabcdef0123456789',
]);
const placeholderTenantCodes = new Set([
'changeme',
'demo',
'example',
'placeholder',
'production-tenant',
'replace-with-tenant-code',
'smoke',
'tenant-production',
'test',
]);
function stringValue(value) {
return String(value || '').trim();
}
function normalizedHostname(value) {
return stringValue(value).toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
}
export function isLoopbackHostname(value) {
const hostname = normalizedHostname(value);
return hostname === 'localhost'
|| hostname.endsWith('.localhost')
|| hostname === '::1'
|| hostname === '0.0.0.0'
|| /^127(?:\.\d{1,3}){3}$/.test(hostname);
}
export function isPlaceholderHostname(value) {
const hostname = normalizedHostname(value);
return ['example', 'test', 'local'].some(suffix => hostname === suffix || hostname.endsWith(`.${suffix}`));
}
export function validateProductionApiBaseUrl(value) {
const apiBaseUrl = stringValue(value);
if (!apiBaseUrl) throw new Error('TARO_APP_API_BASE_URL is required for a production WeApp build');
let parsed;
try {
parsed = new URL(apiBaseUrl);
} catch {
throw new Error('TARO_APP_API_BASE_URL must be an absolute HTTPS URL for a production WeApp build');
}
if (parsed.protocol !== 'https:' || !parsed.hostname) {
throw new Error('TARO_APP_API_BASE_URL must use HTTPS for a production WeApp build');
}
if (isLoopbackHostname(parsed.hostname)) {
throw new Error('TARO_APP_API_BASE_URL must not use localhost or a loopback address for a production WeApp build');
}
if (isPlaceholderHostname(parsed.hostname)) {
throw new Error('TARO_APP_API_BASE_URL must not use a .example, .test, or .local host for a production WeApp build');
}
return apiBaseUrl.replace(/\/+$/, '');
}
export function validateProductionWechatAppId(value) {
const appId = stringValue(value);
if (!appId) throw new Error('WECHAT_MINIAPP_APP_ID is required for a production WeApp build');
if (!/^wx[0-9a-f]{16}$/i.test(appId)) {
throw new Error('WECHAT_MINIAPP_APP_ID must be a real 18-character WeChat AppID');
}
const normalized = appId.toLowerCase();
if (placeholderAppIds.has(normalized) || /^wx([0-9a-f])\1{15}$/i.test(normalized)) {
throw new Error('WECHAT_MINIAPP_APP_ID must not use a placeholder AppID');
}
return appId;
}
export function validateTenantCodeFormat(value) {
const tenantCode = stringValue(value);
if (!tenantCode) throw new Error('TARO_APP_TENANT_CODE is required when TARO_APP_WEAPP_TENANT_MODE=fixed');
if (!/^[A-Za-z0-9._-]{2,64}$/.test(tenantCode)) throw new Error('TARO_APP_TENANT_CODE has an invalid format');
return tenantCode;
}
export function validateProductionTenantCode(value) {
const tenantCode = validateTenantCodeFormat(value);
const normalized = tenantCode.toLowerCase();
if (placeholderTenantCodes.has(normalized)
|| /^(?:tenant[-_])?(?:example|test|demo|smoke|production|placeholder)(?:[-_]tenant)?$/.test(normalized)) {
throw new Error('TARO_APP_TENANT_CODE must not use a placeholder tenant code');
}
return tenantCode;
}
export function resolveWeappTenantMode(env = {}, production = false) {
const configuredMode = stringValue(env.TARO_APP_WEAPP_TENANT_MODE).toLowerCase();
if (configuredMode && configuredMode !== 'fixed' && configuredMode !== 'launch') {
throw new Error('TARO_APP_WEAPP_TENANT_MODE must be fixed or launch');
}
if (configuredMode) return configuredMode;
return production || stringValue(env.TARO_APP_TENANT_CODE) ? 'fixed' : 'launch';
}
export function resolveWeappBuildConfig(env = {}, { production = false } = {}) {
const tenantMode = resolveWeappTenantMode(env, production);
const configuredTenantCode = stringValue(env.TARO_APP_TENANT_CODE);
let tenantCode = '';
if (tenantMode === 'fixed') {
tenantCode = production
? validateProductionTenantCode(configuredTenantCode)
: validateTenantCodeFormat(configuredTenantCode);
} else if (production && configuredTenantCode) {
throw new Error('TARO_APP_TENANT_CODE must be empty when TARO_APP_WEAPP_TENANT_MODE=launch');
}
return {
production,
tenantMode,
tenantCode,
apiBaseUrl: production
? validateProductionApiBaseUrl(env.TARO_APP_API_BASE_URL)
: stringValue(env.TARO_APP_API_BASE_URL),
appId: production ? validateProductionWechatAppId(env.WECHAT_MINIAPP_APP_ID) : '',
};
}
export function runWeappBuild({ argv = process.argv.slice(2), env = process.env } = {}) {
const production = argv.includes('--production');
const buildConfig = resolveWeappBuildConfig(env, { production });
const result = spawnSync(process.execPath, [path.join(repoRoot, 'node_modules', '@tarojs', 'cli', 'bin', 'taro'), 'build', '--type', 'weapp'], {
cwd: taroRoot,
env: {
...env,
TARO_ENV: 'weapp',
TARO_APP_PORTAL: 'student',
TARO_APP_RELEASE_MODE: production ? 'production' : 'preview',
TARO_APP_API_BASE_URL: buildConfig.apiBaseUrl,
TARO_APP_WEAPP_TENANT_MODE: buildConfig.tenantMode,
TARO_APP_TENANT_CODE: buildConfig.tenantCode,
},
stdio: 'inherit',
});
if (result.error) throw result.error;
if (result.status !== 0) return result.status || 1;
if (production) {
const projectConfig = JSON.parse(fs.readFileSync(distProjectConfigPath, 'utf8'));
projectConfig.appid = buildConfig.appId;
projectConfig.setting = { ...(projectConfig.setting || {}), urlCheck: true };
fs.writeFileSync(distProjectConfigPath, `${JSON.stringify(projectConfig, null, 2)}\n`, 'utf8');
}
return 0;
}
if (process.argv[1] && path.resolve(process.argv[1]) === scriptPath) {
try {
process.exitCode = runWeappBuild();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}