const fs = require('fs'); const path = require('path'); const rootDir = path.resolve(__dirname, '..'); const defaultManifestPath = path.join(rootDir, 'dist-electron', 'release-manifest.json'); const descriptorCandidates = ['latest.yml', 'latest-mac.yml', 'latest-linux.yml']; function normalizeVersion(rawValue) { if (typeof rawValue !== 'string') { return null; } const trimmedValue = rawValue.trim(); if (!trimmedValue) { return null; } return trimmedValue.replace(/^v/i, '').split('+')[0] || null; } function parseVersion(rawValue) { const normalized = normalizeVersion(rawValue); if (!normalized) { return null; } const match = normalized.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/); if (!match) { return null; } return { major: Number.parseInt(match[1], 10), minor: Number.parseInt(match[2] || '0', 10), patch: Number.parseInt(match[3] || '0', 10), prerelease: match[4] ? match[4].split('.') : [] }; } function comparePrereleaseIdentifiers(left, right) { const leftNumeric = /^\d+$/.test(left) ? Number.parseInt(left, 10) : null; const rightNumeric = /^\d+$/.test(right) ? Number.parseInt(right, 10) : null; if (leftNumeric !== null && rightNumeric !== null) { return leftNumeric - rightNumeric; } if (leftNumeric !== null) { return -1; } if (rightNumeric !== null) { return 1; } return left.localeCompare(right); } function compareVersions(leftValue, rightValue) { const left = parseVersion(leftValue); const right = parseVersion(rightValue); if (!left && !right) { return 0; } if (!left) { return -1; } if (!right) { return 1; } if (left.major !== right.major) { return left.major - right.major; } if (left.minor !== right.minor) { return left.minor - right.minor; } if (left.patch !== right.patch) { return left.patch - right.patch; } if (left.prerelease.length === 0 && right.prerelease.length === 0) { return 0; } if (left.prerelease.length === 0) { return 1; } if (right.prerelease.length === 0) { return -1; } const maxLength = Math.max(left.prerelease.length, right.prerelease.length); for (let index = 0; index < maxLength; index += 1) { const leftIdentifier = left.prerelease[index]; const rightIdentifier = right.prerelease[index]; if (!leftIdentifier) { return -1; } if (!rightIdentifier) { return 1; } const comparison = comparePrereleaseIdentifiers(leftIdentifier, rightIdentifier); if (comparison !== 0) { return comparison; } } return 0; } function parseArgs(argv) { const args = {}; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; if (!token.startsWith('--')) { continue; } const key = token.slice(2); const nextToken = argv[index + 1]; if (!nextToken || nextToken.startsWith('--')) { args[key] = true; continue; } args[key] = nextToken; index += 1; } return args; } function ensureHttpUrl(rawValue) { if (typeof rawValue !== 'string' || rawValue.trim().length === 0) { throw new Error('A release feed URL is required. Pass it with --feed-url.'); } const parsedUrl = new URL(rawValue.trim()); if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { throw new Error('The release feed URL must use http:// or https://.'); } return parsedUrl.toString().replace(/\/$/, ''); } function readJsonIfExists(filePath) { if (!fs.existsSync(filePath)) { return null; } return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function warnIfNoDescriptors(distPath) { const availableDescriptors = descriptorCandidates.filter((fileName) => { return fs.existsSync(path.join(distPath, fileName)); }); if (availableDescriptors.length === 0) { console.warn('[release-manifest] Warning: no latest*.yml descriptor was found in dist-electron/.'); console.warn('[release-manifest] Upload a feed directory that contains the Electron Builder update descriptors.'); } } function buildDefaultManifest(version) { return { schemaVersion: 1, generatedAt: new Date().toISOString(), minimumServerVersion: version, pollIntervalMinutes: 30, versions: [] }; } function sortManifestVersions(versions) { return [...versions].sort((left, right) => compareVersions(right.version, left.version)); } function main() { const args = parseArgs(process.argv.slice(2)); const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')); const version = normalizeVersion(args.version || packageJson.version); if (!version) { throw new Error('Unable to determine the release version. Pass it with --version.'); } const feedUrl = ensureHttpUrl(args['feed-url']); const manifestPath = path.resolve(rootDir, args.manifest || defaultManifestPath); const existingPath = path.resolve(rootDir, args.existing || manifestPath); const existingManifest = readJsonIfExists(existingPath) || buildDefaultManifest(version); const distPath = path.join(rootDir, 'dist-electron'); if (fs.existsSync(distPath)) { warnIfNoDescriptors(distPath); } const nextEntry = { version, feedUrl, publishedAt: typeof args['published-at'] === 'string' ? args['published-at'] : new Date().toISOString(), ...(typeof args.notes === 'string' && args.notes.trim().length > 0 ? { notes: args.notes.trim() } : {}) }; const nextManifest = { schemaVersion: 1, generatedAt: new Date().toISOString(), minimumServerVersion: normalizeVersion(args['minimum-server-version']) || normalizeVersion(existingManifest.minimumServerVersion) || version, pollIntervalMinutes: Number.isFinite(Number(args['poll-interval-minutes'])) ? Math.max(5, Math.round(Number(args['poll-interval-minutes']))) : Math.max(5, Math.round(Number(existingManifest.pollIntervalMinutes || 30))), versions: sortManifestVersions( [...(Array.isArray(existingManifest.versions) ? existingManifest.versions : [])] .filter((entry) => normalizeVersion(entry && entry.version) !== version) .concat(nextEntry) ) }; fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); fs.writeFileSync(manifestPath, `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8'); console.log(`[release-manifest] Wrote ${manifestPath}`); } try { main(); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`[release-manifest] ${message}`); process.exitCode = 1; }