forked from wangziqi/gongxue-base
feat: establish production SaaS foundation
This commit is contained in:
173
scripts/build-weapp-student.js
Normal file
173
scripts/build-weapp-student.js
Normal file
@@ -0,0 +1,173 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user