import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' import semver from 'semver' export const CONFIG_FILE_NAME = 'xuqm.config.json' export const SUPPORTED_PLATFORMS = new Set(['android', 'ios']) export const SUPPORTED_MODULE_TYPES = new Set(['startup', 'common', 'app', 'buz']) export function readConfig(root = process.cwd()) { const configFile = path.join(root, CONFIG_FILE_NAME) if (!existsSync(configFile)) { throw new Error(`missing ${CONFIG_FILE_NAME}; run \`xuqm-rn init\` first`) } let config try { config = JSON.parse(readFileSync(configFile, 'utf8')) } catch (error) { throw new Error(`invalid ${CONFIG_FILE_NAME}: ${error.message}`) } if (config.schemaVersion !== 3) { throw new Error(`${CONFIG_FILE_NAME} schemaVersion must be 3`) } return config } export function validateConfig(config, root = process.cwd()) { const errors = [] if (!config.appId) errors.push('appId is required') if (!config.mainModuleName) errors.push('mainModuleName is required') if (typeof config.pluginVersion !== 'string' || !semver.valid(config.pluginVersion)) { errors.push('pluginVersion must be SemVer') } if (typeof config.appVersionRange !== 'string' || !semver.validRange(config.appVersionRange)) { errors.push('appVersionRange must be a valid SemVer range') } if (!Array.isArray(config.modules) || config.modules.length === 0) { errors.push('modules must contain at least one module') return errors } if (config.modules.length > 1 && !config.metroConfig) { errors.push('metroConfig is required for multi-module builds') } if (config.metroConfig && !existsSync(path.resolve(root, config.metroConfig))) { errors.push(`Metro config not found: ${config.metroConfig}`) } const ids = new Set() for (const module of config.modules) { if (!/^[a-z][a-z0-9-]*$/.test(module.id ?? '')) { errors.push(`invalid module id: ${module.id ?? ''}`) } if (ids.has(module.id)) errors.push(`duplicate module id: ${module.id}`) ids.add(module.id) if (!SUPPORTED_MODULE_TYPES.has(module.type)) { errors.push(`invalid module type for ${module.id}: ${module.type ?? ''}`) } if (!module.entry || !existsSync(path.resolve(root, module.entry))) { errors.push(`module entry not found for ${module.id}: ${module.entry ?? ''}`) } if (module.moduleVersion !== undefined && typeof module.moduleVersion !== 'string') { errors.push(`moduleVersion must be a string for ${module.id}`) } else if (module.moduleVersion !== undefined && !semver.valid(module.moduleVersion)) { errors.push(`moduleVersion must be SemVer for ${module.id}`) } if (['app', 'buz'].includes(module.type) && module.commonVersionRange !== undefined) { if ( typeof module.commonVersionRange !== 'string' || !semver.validRange(module.commonVersionRange) ) { errors.push(`commonVersionRange must be a valid SemVer range for ${module.id}`) } } if ( module.minNativeApiLevel !== undefined && (!Number.isInteger(module.minNativeApiLevel) || module.minNativeApiLevel < 1) ) { errors.push(`minNativeApiLevel must be a positive integer for ${module.id}`) } if (module.metroConfig && !existsSync(path.resolve(root, module.metroConfig))) { errors.push(`Metro config not found for ${module.id}: ${module.metroConfig}`) } if (module.ownershipRoots !== undefined) { if (module.type !== 'common') { errors.push(`ownershipRoots is only supported by the common module: ${module.id}`) } else if (!Array.isArray(module.ownershipRoots) || module.ownershipRoots.length === 0) { errors.push(`ownershipRoots must be a non-empty string array for ${module.id}`) } else { for (const ownershipRoot of module.ownershipRoots) { if ( typeof ownershipRoot !== 'string' || ownershipRoot.trim() === '' || !existsSync(path.resolve(root, ownershipRoot)) ) { errors.push( `ownership root not found for ${module.id}: ${ownershipRoot ?? ''}`, ) } } } } } return errors } export function readPackageVersion(root = process.cwd()) { const packageFile = path.join(root, 'package.json') if (!existsSync(packageFile)) throw new Error('package.json is missing') const pkg = JSON.parse(readFileSync(packageFile, 'utf8')) if (typeof pkg.version !== 'string' || pkg.version.trim() === '') { throw new Error('package.json version must be a non-empty string') } return pkg.version.trim() } export function compatibleAppRange(appVersion) { const parsed = semver.parse(appVersion) if (!parsed) throw new Error(`app version must be SemVer: ${appVersion}`) return `>=${parsed.version} <${parsed.major + 1}.0.0` } export function resolveVersionedModules(config, root = process.cwd(), appVersionOverride) { const appVersion = appVersionOverride?.trim() || readPackageVersion(root) const pluginVersion = semver.valid(config.pluginVersion) if (!pluginVersion) throw new Error('pluginVersion must be SemVer') const common = config.modules.find(module => module.type === 'common') const commonVersion = common?.moduleVersion ?? pluginVersion const commonSemVer = semver.parse(commonVersion) if (!commonSemVer) throw new Error(`common module version must be SemVer: ${commonVersion}`) const defaultCommonRange = `>=${commonSemVer.version} <${commonSemVer.major + 1}.0.0` return { appVersion, modules: config.modules.map(module => ({ ...module, moduleVersion: module.moduleVersion ?? pluginVersion, appVersionRange: module.appVersionRange ?? config.appVersionRange, ...(module.commonVersionRange === undefined && ['app', 'buz'].includes(module.type) ? { commonVersionRange: defaultCommonRange } : {}), minNativeApiLevel: module.minNativeApiLevel ?? 2, })), } } export function selectModules(config, moduleIds = []) { if (moduleIds.length === 0) return config.modules const requested = new Set(moduleIds) const selected = config.modules.filter(module => requested.has(module.id)) const found = new Set(selected.map(module => module.id)) const missing = [...requested].filter(id => !found.has(id)) if (missing.length) throw new Error(`unknown module(s): ${missing.join(', ')}`) return selected } export function bundleOutputDirectory(root, config, platform, moduleId) { return path.resolve(root, config.outputDir ?? './bundle', platform, moduleId) } export function bundleFile(root, config, platform, moduleId) { return path.join( bundleOutputDirectory(root, config, platform, moduleId), `${moduleId}.${platform}.bundle`, ) } export function parseModuleOptions(args) { const moduleIds = [] const remaining = [] for (let index = 0; index < args.length; index += 1) { if (args[index] === '--module') { const value = args[index + 1] if (!value) throw new Error('--module requires a module id') moduleIds.push( ...value .split(',') .map(id => id.trim()) .filter(Boolean), ) index += 1 } else { remaining.push(args[index]) } } return { moduleIds: [...new Set(moduleIds)], remaining } }