mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
c7d71ea57c
* feat(whats-new): remote release notes with dev workspace mode Add WHATS_NEW.md, CI whats-new.md asset upload, and client fetch/cache with embedded fallbacks. Dev and -dev builds read the full file from the repo for debugging; RC/stable download the release asset on first use. * fix(whats-new): render ## headings and add changelog tab Parse h2 sections in release-notes markdown; load changelog alongside highlights and let users switch views on the What's New page. * fix(whats-new): prefetch on startup and fix CI typecheck prebuild Prefetch whats-new asset when the shell loads on RC/stable builds. Run prebuild:release-notes before tsc and coverage jobs so the gitignored generated bundle exists in CI. * docs: CHANGELOG and credits for What's New remote notes (PR #1058) * fix(whats-new): always slice embedded release notes to current line Drop full CHANGELOG embed for -dev bundles; tauri:dev still reads live markdown from the repo. Ignore all of src/generated/ in git. * fix(whats-new): fetch release asset via Rust to bypass CORS Route whats-new.md download through fetch_url_bytes; rename the technical tab label; add fetch unit tests (PR #1058 review).
92 lines
2.5 KiB
JavaScript
92 lines
2.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Extract the body of a ## [version] section from a Keep-a-Changelog-style file.
|
|
* Resolution matches src/utils/releaseNotes/releaseNotesMatch.ts.
|
|
*
|
|
* Usage: node scripts/extract-release-section.mjs <file> <version> [--allow-empty]
|
|
* Stdout: section body (no ## header). Exit 1 if empty unless --allow-empty.
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
const SEMVER_CORE = /^v?(\d+\.\d+\.\d+)/i;
|
|
|
|
function versionCore(version) {
|
|
const m = version.trim().match(SEMVER_CORE);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
function isPlainTriple(header) {
|
|
return /^\d+\.\d+\.\d+$/.test(header.trim());
|
|
}
|
|
|
|
function splitBlocks(raw) {
|
|
return raw.split(/\n(?=## \[)/).filter((b) => b.startsWith('## ['));
|
|
}
|
|
|
|
function headerVersion(block) {
|
|
const m = block.match(/^## \[([^\]]+)\]/);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
function parseBlock(block) {
|
|
const lines = block.split('\n');
|
|
const m = lines[0].match(/## \[([^\]]+)\](?:\s*-\s*(.+))?/);
|
|
if (!m) return null;
|
|
return {
|
|
headerVersion: m[1],
|
|
date: (m[2] ?? '').trim(),
|
|
body: lines.slice(1).join('\n').trim(),
|
|
};
|
|
}
|
|
|
|
export function findReleaseSection(raw, appVersion) {
|
|
const blocks = splitBlocks(raw);
|
|
|
|
const exact = blocks.find((b) => b.startsWith(`## [${appVersion}]`));
|
|
if (exact) return parseBlock(exact);
|
|
|
|
const appCore = versionCore(appVersion);
|
|
if (!appCore) return null;
|
|
|
|
const candidates = blocks.filter((b) => {
|
|
const hv = headerVersion(b);
|
|
return hv !== null && versionCore(hv) === appCore;
|
|
});
|
|
if (candidates.length === 0) return null;
|
|
|
|
const plain = candidates.find((b) => {
|
|
const hv = headerVersion(b);
|
|
return hv !== null && isPlainTriple(hv);
|
|
});
|
|
return parseBlock(plain ?? candidates[0]);
|
|
}
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
const allowEmpty = args.includes('--allow-empty');
|
|
const positional = args.filter((a) => a !== '--allow-empty');
|
|
const [file, version] = positional;
|
|
|
|
if (!file || !version) {
|
|
console.error('Usage: node scripts/extract-release-section.mjs <file> <version> [--allow-empty]');
|
|
process.exit(2);
|
|
}
|
|
|
|
const raw = readFileSync(file, 'utf8');
|
|
const entry = findReleaseSection(raw, version);
|
|
const body = entry?.body?.trim() ?? '';
|
|
|
|
if (!body) {
|
|
if (allowEmpty) process.exit(0);
|
|
console.error(`No release section found in ${file} for version ${version}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write(`${body}\n`);
|
|
}
|
|
|
|
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
if (isMain) main();
|