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; } }