6.7.0 - Alpha6 - Scrapers 工具的更新机制由 "锚点更新" 替换为 "结构化更新"
This commit is contained in:
@@ -1,266 +0,0 @@
|
||||
// utils/anchors.mjs
|
||||
|
||||
import * as fsp from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { escapeRegExp } from './format.mjs';
|
||||
import { toUpdatedStamp } from './date.mjs';
|
||||
import { printDifferences } from './print.mjs';
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @returns {string}
|
||||
*/
|
||||
const normalize = (s) => String(s).replace(/\s+/g, '');
|
||||
|
||||
/**
|
||||
* Generate new block content with the given replacement function in the specified Anchor block.<br>
|
||||
* zh-CN: 在指定 Anchor 块中, 用给定的替换函数生成新块内容.
|
||||
*
|
||||
* @param {string} src
|
||||
* @param {string} anchorTag
|
||||
* @param {(block: string) => { newBlock: string, changed: boolean }} replaceBlockFn
|
||||
* @returns {{ src: string, changed: boolean }}
|
||||
*/
|
||||
function replaceInAnchoredBlock(src, anchorTag, replaceBlockFn) {
|
||||
const beginTag = `// @AnchorBegin ${anchorTag}`;
|
||||
const endTag = `// @AnchorEnd ${anchorTag}`;
|
||||
|
||||
const beginIdx = src.indexOf(beginTag);
|
||||
if (beginIdx === -1) throw new Error(`Anchor tag "${anchorTag}" not found in the source code`);
|
||||
|
||||
const endIdx = src.indexOf(endTag, beginIdx + beginTag.length);
|
||||
if (endIdx === -1) throw new Error(`Anchor tag "${anchorTag}" not found in the source code`);
|
||||
|
||||
const before = src.slice(0, beginIdx);
|
||||
const block = src.slice(beginIdx, endIdx);
|
||||
const after = src.slice(endIdx);
|
||||
|
||||
const { newBlock, changed } = replaceBlockFn(block) || {};
|
||||
if (!changed || !newBlock) return { src, changed: false };
|
||||
|
||||
return { src: before + newBlock + after, changed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a map declaration (like mapOf(...)) in an anchor block
|
||||
* and automatically refresh the @Updated date when changed.<br>
|
||||
* zh-CN: 替换锚点块中的某个 map 声明 (如 mapOf(...)), 并在变更时自动刷新 @Updated 日期.
|
||||
*
|
||||
* @param {string} src
|
||||
* @param {Object} options
|
||||
* @param {string} options.anchorTag
|
||||
* @param {string} options.mapName
|
||||
* @param {string[]} options.lines
|
||||
* @param {number} [options.linesIndent=4]
|
||||
* @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp]
|
||||
* @returns {{ src: string, changed: boolean }}
|
||||
*/
|
||||
function replaceAnchoredMapBlock(src, {
|
||||
anchorTag,
|
||||
mapName,
|
||||
lines,
|
||||
linesIndent = 4,
|
||||
toUpdatedStamp: toStamp = toUpdatedStamp,
|
||||
}) {
|
||||
return replaceInAnchoredBlock(src, anchorTag, (block) => {
|
||||
let changed = false;
|
||||
|
||||
const re = new RegExp(String.raw`([\t ]*)(va[lr]\s+)?${escapeRegExp(mapName)}\s*=\s*mapOf\(.*?\)(,?)`, 's');
|
||||
|
||||
let updatedBlock = block.replace(re, (/** @type {string} */ original, /** @type {string} */ indent, /** @type {string} */ keyword, /** @type {string} */ comma) => {
|
||||
const kw = keyword ?? '';
|
||||
const body = lines.map(l => `${' '.repeat(linesIndent)}${indent}${l}`).join('\n');
|
||||
const next = `${indent}${kw}${mapName} = mapOf(\n${body}\n${indent})${comma}`;
|
||||
if (normalize(original) !== normalize(next)) changed = true;
|
||||
return next;
|
||||
});
|
||||
if (changed) {
|
||||
updatedBlock = updatedBlock.replace(
|
||||
/(@Updated[^\n]*?\son\s)([A-Z][a-z]{2}\s\d{1,2},\s\d{4})(\.?)/,
|
||||
(_, p1, _old, p3) => `${p1}${(toStamp())}${p3}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { newBlock: updatedBlock, changed };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a list declaration (like listOf(...)) in an anchor block
|
||||
* and automatically refresh the @Updated date when changed.<br>
|
||||
* zh-CN:<br>
|
||||
* 替换锚点块中的某个 list 声明 (如 listOf(...)), 并在变更时自动刷新 @Updated 日期.
|
||||
*
|
||||
* @param {string} src
|
||||
* @param {Object} options
|
||||
* @param {string} options.anchorTag
|
||||
* @param {string} options.listName
|
||||
* @param {string[]} options.lines
|
||||
* @param {number} [options.linesIndent=4]
|
||||
* @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp]
|
||||
* @returns {{ src: string, changed: boolean }}
|
||||
*/
|
||||
function replaceAnchoredListBlock(src, {
|
||||
anchorTag,
|
||||
listName,
|
||||
lines,
|
||||
linesIndent = 4,
|
||||
toUpdatedStamp: toStamp = toUpdatedStamp,
|
||||
}) {
|
||||
return replaceInAnchoredBlock(src, anchorTag, (block) => {
|
||||
let changed = false;
|
||||
|
||||
const re = new RegExp(`([\\t\\x20]*)(va[lr]\\s+)?${listName}\\s*=\\s*listOf\\([\\s\\S]*?\\)(,?)`, 'm');
|
||||
let updatedBlock = block.replace(re, (original, indent, keyword, comma) => {
|
||||
const kw = keyword ?? '';
|
||||
const body = lines.map(l => `${' '.repeat(linesIndent)}${indent}${l}`).join('\n');
|
||||
const next = `${indent}${kw}${listName} = listOf(\n${body}\n${indent})${comma}`;
|
||||
if (normalize(original) !== normalize(next)) changed = true;
|
||||
return next;
|
||||
});
|
||||
if (changed) {
|
||||
updatedBlock = updatedBlock.replace(
|
||||
/(@Updated[^\n]*?\son\s)([A-Z][a-z]{2}\s\d{1,2},\s\d{4})(\.?)/,
|
||||
(_, p1, _old, p3) => `${p1}${(toStamp())}${p3}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { newBlock: updatedBlock, changed };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filePath
|
||||
* @param {Object} options
|
||||
* @param {string} options.anchorTag
|
||||
* @param {string} options.mapName
|
||||
* @param {string[]} options.lines
|
||||
* @param {number} [options.linesIndent=4]
|
||||
* @param {string} [options.updatedLabel='']
|
||||
* @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp]
|
||||
* @param {RegExp} [options.regexForKeyMatching=null]
|
||||
* @returns {Promise<{ changed: boolean, content: string }>}
|
||||
*/
|
||||
export async function updateAnchoredMapInFile(filePath, {
|
||||
anchorTag,
|
||||
mapName,
|
||||
lines,
|
||||
linesIndent = 4,
|
||||
updatedLabel = '',
|
||||
toUpdatedStamp: toStamp = toUpdatedStamp,
|
||||
regexForKeyMatching = null,
|
||||
}) {
|
||||
const filename = path.basename(filePath);
|
||||
const raw = await fsp.readFile(filePath, 'utf8');
|
||||
const { src: updated, changed } = replaceAnchoredMapBlock(raw, { anchorTag, mapName, lines, linesIndent, toUpdatedStamp: toStamp });
|
||||
|
||||
if (changed) {
|
||||
await fsp.writeFile(filePath, updated, 'utf8');
|
||||
console.log(`[${filename}] Updated` + (updatedLabel ? ` (${updatedLabel})` : ''));
|
||||
printDifferences(raw, updated, { regexForKeyMatching });
|
||||
} else {
|
||||
// console.log(`[${filename}] No update needed` + (updatedLabel ? ` (${updatedLabel})` : ''));
|
||||
}
|
||||
return { changed, content: updated };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filePath
|
||||
* @param {Object} options
|
||||
* @param {string} options.anchorTag
|
||||
* @param {string} options.listName
|
||||
* @param {string[]} options.lines
|
||||
* @param {number} [options.linesIndent=4]
|
||||
* @param {string} [options.updatedLabel='']
|
||||
* @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp]
|
||||
* @param {RegExp} [options.regexForKeyMatching=null]
|
||||
* @returns {Promise<{ changed: boolean, content: string }>}
|
||||
*/
|
||||
export async function updateAnchoredListInFile(filePath, {
|
||||
anchorTag,
|
||||
listName,
|
||||
lines,
|
||||
linesIndent = 4,
|
||||
updatedLabel = '',
|
||||
toUpdatedStamp: toStamp = toUpdatedStamp,
|
||||
regexForKeyMatching = null,
|
||||
}) {
|
||||
const filename = path.basename(filePath);
|
||||
const raw = await fsp.readFile(filePath, 'utf8');
|
||||
const { src: updated, changed } = replaceAnchoredListBlock(raw, { anchorTag, listName, lines, linesIndent, toUpdatedStamp: toStamp });
|
||||
|
||||
if (changed) {
|
||||
await fsp.writeFile(filePath, updated, 'utf8');
|
||||
console.log(`[${filename}] Updated` + (updatedLabel ? ` (${updatedLabel})` : ''));
|
||||
printDifferences(raw, updated, { regexForKeyMatching });
|
||||
} else {
|
||||
// console.log(`[${filename}] No update needed` + (updatedLabel ? ` (${updatedLabel})` : ''));
|
||||
}
|
||||
return { changed, content: updated };
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch replace multiple anchors within the same file
|
||||
* (supports both map and list, requiring only one read/write operation).<br>
|
||||
* zh-CN: 批量在同一文件内进行多锚点替换 (同时支持 map 与 list, 读写仅需一次).
|
||||
*
|
||||
* @param {string} filePath
|
||||
* @param {AnchoredBlockUpdateOption[]} optionList
|
||||
* @param {Object} [extraOptions={}]
|
||||
* @param {(date?: Date) => string} [extraOptions.toUpdatedStamp=toUpdatedStamp]
|
||||
* @param {RegExp} [extraOptions.regexForKeyMatching=null]
|
||||
* @returns {Promise<{ changed: boolean, content: string }>}
|
||||
*/
|
||||
export async function batchUpdateAnchoredBlocks(filePath, optionList, {
|
||||
toUpdatedStamp: toStamp = toUpdatedStamp,
|
||||
regexForKeyMatching = null,
|
||||
} = {}) {
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
let raw = await fsp.readFile(filePath, 'utf8');
|
||||
let changedAny = false;
|
||||
let updated = null;
|
||||
let updatedLabel = null;
|
||||
|
||||
for (const opt of optionList) {
|
||||
let res = { src: raw, changed: false };
|
||||
|
||||
if (opt.type === 'map') {
|
||||
res = replaceAnchoredMapBlock(raw, {
|
||||
anchorTag: opt.anchorTag,
|
||||
mapName: opt.mapName,
|
||||
lines: opt.lines,
|
||||
linesIndent: opt.linesIndent,
|
||||
toUpdatedStamp: toStamp,
|
||||
});
|
||||
} else if (opt.type === 'list') {
|
||||
res = replaceAnchoredListBlock(raw, {
|
||||
anchorTag: opt.anchorTag,
|
||||
listName: opt.listName,
|
||||
lines: opt.lines,
|
||||
linesIndent: opt.linesIndent,
|
||||
toUpdatedStamp: toStamp,
|
||||
});
|
||||
} else if (opt.type === 'custom' && typeof opt.replacer === 'function') {
|
||||
res = replaceInAnchoredBlock(raw, opt.anchorTag, (block) => opt.replacer(block, { toUpdatedStamp: toStamp }));
|
||||
} else {
|
||||
console.warn(`[${filename}] Unknown operation type or missing parameters:`, opt);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (res.changed) {
|
||||
changedAny = true;
|
||||
updated = res.src;
|
||||
updatedLabel = opt.updatedLabel;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedAny) {
|
||||
await fsp.writeFile(filePath, updated, 'utf8');
|
||||
console.log(`[${filename}] Updated` + (updatedLabel ? ` (${updatedLabel})` : ''));
|
||||
printDifferences(raw, updated, { regexForKeyMatching });
|
||||
} else {
|
||||
// console.log(`[${filename}] No update needed (${op['updatedLabel'] ?? op.anchorTag})`);
|
||||
}
|
||||
return { changed: changedAny, content: changedAny ? updated : raw };
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const findCommonSet = (a1, a2) => {
|
||||
* @param {RegExp} [options.regexForKeyMatching]
|
||||
* @return {void}
|
||||
*/
|
||||
export function printDifferences(src, other, options = {}) {
|
||||
export function printLinesDiffs(src, other, options = {}) {
|
||||
const leftRaw = extractConcernedLines(src);
|
||||
const rightRaw = extractConcernedLines(other);
|
||||
const commonSet = findCommonSet(leftRaw, rightRaw);
|
||||
@@ -134,3 +134,27 @@ export function printDifferences(src, other, options = {}) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string,string>} src
|
||||
* @param {Map<string,string>} other
|
||||
*/
|
||||
export function printMapDiffs(src, other) {
|
||||
printLinesDiffs(mapToLines(src), mapToLines(other));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} src
|
||||
* @param {string[]} other
|
||||
*/
|
||||
export function printListDiffs(src, other) {
|
||||
printLinesDiffs(src.join('\n'), other.join('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string,string>} map
|
||||
* @returns {string}
|
||||
*/
|
||||
function mapToLines(map) {
|
||||
return Array.from(map.entries()).map(([ k, v ]) => `"${k}" to "${v}"`).join('\n');
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import * as fs from 'node:fs';
|
||||
import * as fsp from 'node:fs/promises';
|
||||
import { compareVersionStrings } from './versioning.mjs';
|
||||
import { generatePropertiesFileTimestamp } from './date.mjs';
|
||||
import { sortByMap } from './sorting.mjs';
|
||||
|
||||
/**
|
||||
* Convert JS string to .properties format escaping rules (store format)
|
||||
@@ -159,10 +160,11 @@ function unescapeProperty(str) {
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {Object<string, string>}
|
||||
* @param {MapSortable['sort']} [sortingPattern=null]
|
||||
* @returns {Map<string, string>}
|
||||
*/
|
||||
function parseProperties(text) {
|
||||
const props = Object.create(null);
|
||||
export function parseProperties(text, sortingPattern = null) {
|
||||
const props = new Map();
|
||||
if (!text) return props;
|
||||
|
||||
const lines = [];
|
||||
@@ -243,75 +245,115 @@ function parseProperties(text) {
|
||||
const k = unescapeProperty(key);
|
||||
const v = unescapeProperty(value.trim());
|
||||
|
||||
if (k) props[k] = v;
|
||||
if (k) props.set(k, v);
|
||||
}
|
||||
return props;
|
||||
return new Map(sortByMap(props.entries(), sortingPattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [filePath='../version.properties']
|
||||
* @param {Object} options
|
||||
* @param {BufferEncoding} [options.encoding='utf8']
|
||||
* @returns {Promise<Object<string, string>>}
|
||||
* @param {GradleMapRwOptions} [options={}]
|
||||
* @returns {Promise<Map<string, string>>}
|
||||
*/
|
||||
export async function readProperties(filePath = '../version.properties', { encoding = 'utf8' } = {}) {
|
||||
export async function readProperties(filePath = '../version.properties', { encoding = 'utf8', sort = null } = {}) {
|
||||
const text = await fsp.readFile(filePath, { encoding });
|
||||
return parseProperties(text);
|
||||
return parseProperties(text, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [filePath='../version.properties']
|
||||
* @param {Object} options
|
||||
* @param {BufferEncoding} [options.encoding='utf8']
|
||||
* @returns {Object<string, string>}
|
||||
* @param {GradleMapRwOptions} [options={}]
|
||||
* @returns {Map<string, string>}
|
||||
*/
|
||||
export function readPropertiesSync(filePath = '../version.properties', { encoding = 'utf8' } = {}) {
|
||||
export function readPropertiesSync(filePath = '../version.properties', { encoding = 'utf8', sort = null } = {}) {
|
||||
const text = fs.readFileSync(filePath, { encoding });
|
||||
return parseProperties(text);
|
||||
return parseProperties(text, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [filePath='../version.properties']]
|
||||
* @param {Object<string,string>} [props={}]
|
||||
* @param {Object} options
|
||||
* @param {BufferEncoding} [options.encoding='utf8']
|
||||
* @param {string} [filePath='../version.properties']
|
||||
* @param {Map<string,string>} [map={}]
|
||||
* @param {GradleMapRwOptions} [options={}]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function writeProperties(filePath = '../version.properties', props = {}, { encoding = 'utf8' } = {}) {
|
||||
export async function writePropertiesWithMap(filePath = '../version.properties', map = new Map(), { encoding = 'utf8', sort = null } = {}) {
|
||||
const lines = [];
|
||||
for (const key in props) {
|
||||
const value = props[key];
|
||||
if (value == null) continue;
|
||||
sortByMap(map.entries(), sort).forEach(([ key, value ]) => {
|
||||
if (value == null) return;
|
||||
const k = escapeProperty(String(key), true);
|
||||
const v = escapeProperty(String(value), false);
|
||||
lines.push(`${k}=${v}`);
|
||||
}
|
||||
});
|
||||
lines.unshift(generatePropertiesFileTimestamp());
|
||||
const text = lines.join('\n') + '\n';
|
||||
return fsp.writeFile(filePath, text, { encoding });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [filePath='../version.properties']]
|
||||
* @param {Object<string,string>} [props={}]
|
||||
* @param {Object} options
|
||||
* @param {BufferEncoding} [options.encoding='utf8']
|
||||
* @param {string} [filePath='../version.properties']
|
||||
* @param {Map<string,string>} [map={}]
|
||||
* @param {GradleMapRwOptions} [options={}]
|
||||
* @returns {void}
|
||||
*/
|
||||
export function writePropertiesSync(filePath = '../version.properties', props = {}, { encoding = 'utf8' } = {}) {
|
||||
export function writePropertiesSyncWithMap(filePath = '../version.properties', map = new Map(), { encoding = 'utf8', sort = null } = {}) {
|
||||
const lines = [];
|
||||
for (const key in props) {
|
||||
const value = props[key];
|
||||
if (value == null) continue;
|
||||
sortByMap(map.entries(), sort).forEach(([ key, value ]) => {
|
||||
if (value == null) return;
|
||||
const k = escapeProperty(String(key), true);
|
||||
const v = escapeProperty(String(value), false);
|
||||
lines.push(`${k}=${v}`);
|
||||
}
|
||||
});
|
||||
lines.unshift(generatePropertiesFileTimestamp());
|
||||
const text = lines.join('\n') + '\n';
|
||||
return fs.writeFileSync(filePath, text, { encoding });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [filePath='../version.properties']
|
||||
* @param {string[]} [lines=[]]
|
||||
* @param {GradleDataRwOptions} [options={}]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function writePropertiesWithLines(filePath = '../version.properties', lines = [], { encoding = 'utf8' } = {}) {
|
||||
const results = [];
|
||||
lines.forEach((line) => {
|
||||
if (line.startsWith('#') || line.startsWith('!')) {
|
||||
results.push(line);
|
||||
}
|
||||
const [ key, value ] = parseProperties(line).entries().next().value || [];
|
||||
if (value == null) return;
|
||||
const k = escapeProperty(String(key), true);
|
||||
const v = escapeProperty(String(value), false);
|
||||
results.push(`${k}=${v}`);
|
||||
});
|
||||
results.unshift(generatePropertiesFileTimestamp());
|
||||
const text = results.join('\n') + '\n';
|
||||
return fsp.writeFile(filePath, text, { encoding });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [filePath='../version.properties']
|
||||
* @param {string[]} [lines=[]]
|
||||
* @param {GradleDataRwOptions} [options={}]
|
||||
* @returns {void}
|
||||
*/
|
||||
export function writePropertiesSyncWithLines(filePath = '../version.properties', lines = [], { encoding = 'utf8' } = {}) {
|
||||
const results = [];
|
||||
lines.forEach((line) => {
|
||||
if (line.startsWith('#') || line.startsWith('!')) {
|
||||
results.push(line);
|
||||
}
|
||||
const [ key, value ] = parseProperties(line).entries().next().value || [];
|
||||
if (value == null) return;
|
||||
const k = escapeProperty(String(key), true);
|
||||
const v = escapeProperty(String(value), false);
|
||||
results.push(`${k}=${v}`);
|
||||
});
|
||||
results.unshift(generatePropertiesFileTimestamp());
|
||||
const text = results.join('\n') + '\n';
|
||||
return fs.writeFileSync(filePath, text, { encoding });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [filePath='../version.properties']
|
||||
* @param {Object} options
|
||||
@@ -320,7 +362,7 @@ export function writePropertiesSync(filePath = '../version.properties', props =
|
||||
*/
|
||||
export function getMinSupportedAgpVersion(filePath = '../version.properties', { encoding = 'utf8' } = {}) {
|
||||
let minSupportedVersion = null;
|
||||
Object.entries(readPropertiesSync(filePath, { encoding })).forEach(([ key, value ]) => {
|
||||
Array.from(readPropertiesSync(filePath, { encoding }).entries()).forEach(([ key, value ]) => {
|
||||
if (!/agp.version.*min.supported|min.supported.*agp.version/i.test(key)) return;
|
||||
if (minSupportedVersion === null || compareVersionStrings(value, minSupportedVersion) < 0) {
|
||||
minSupportedVersion = value;
|
||||
@@ -340,7 +382,7 @@ export function getMinSupportedAgpVersion(filePath = '../version.properties', {
|
||||
*/
|
||||
export function getMinSupportedGradleVersion(filePath = '../version.properties', { encoding = 'utf8' } = {}) {
|
||||
let minSupportedVersion = null;
|
||||
Object.entries(readPropertiesSync(filePath, { encoding })).forEach(([ key, value ]) => {
|
||||
Array.from(readPropertiesSync(filePath, { encoding }).entries()).forEach(([ key, value ]) => {
|
||||
if (!/gradle.version.*min.supported|min.supported.*gradle.version/i.test(key)) return;
|
||||
if (minSupportedVersion === null || compareVersionStrings(value, minSupportedVersion) < 0) {
|
||||
minSupportedVersion = value;
|
||||
@@ -373,7 +415,7 @@ export function getJavaVersionInfo(filePath = '../version.properties', { encodin
|
||||
let minSupportedVer = Infinity;
|
||||
let maxSupportedVer = -Infinity;
|
||||
|
||||
Object.entries(readPropertiesSync(filePath, { encoding })).forEach(([ key, value ]) => {
|
||||
Array.from(readPropertiesSync(filePath, { encoding }).entries()).forEach(([ key, value ]) => {
|
||||
const currentVer = parseInt(value, 10);
|
||||
if (/java.version.*min.suggested|min.suggested.*java.version/i.test(key)) {
|
||||
minSuggestedVer = Math.min(minSuggestedVer, currentVer);
|
||||
|
||||
75
.utils/utils/sorting.mjs
Normal file
75
.utils/utils/sorting.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
// utils/sorting.mjs
|
||||
|
||||
import { compareVersionStrings } from './versioning.mjs';
|
||||
|
||||
/**
|
||||
* @param {[string, string][] | MapIterator<[string, string]> | Map<string, string>} src
|
||||
* @param {MapSortable['sort'] | null} [pattern=null]
|
||||
* @returns {[string, string][]}
|
||||
*/
|
||||
export function sortByMap(src, pattern = null) {
|
||||
const entries = src instanceof Map ? Array.from(src.entries()) : Array.from(src);
|
||||
switch (pattern) {
|
||||
case null:
|
||||
return entries;
|
||||
case 'key.ascending':
|
||||
case 'key.ascending.as.string':
|
||||
return entries.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
case 'key.ascending.as.number':
|
||||
return entries.sort((a, b) => Number(a[0]) - Number(b[0]));
|
||||
case 'key.ascending.as.version':
|
||||
return entries.sort((a, b) => compareVersionStrings(a[0], b[0]));
|
||||
case 'key.descending':
|
||||
case 'key.descending.as.string':
|
||||
return entries.sort((a, b) => b[0].localeCompare(a[0]));
|
||||
case 'key.descending.as.number':
|
||||
return entries.sort((a, b) => Number(b[0]) - Number(a[0]));
|
||||
case 'key.descending.as.version':
|
||||
return entries.sort((a, b) => compareVersionStrings(b[0], a[0]));
|
||||
case 'value.ascending':
|
||||
case 'value.ascending.as.string':
|
||||
return entries.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
case 'value.ascending.as.number':
|
||||
return entries.sort((a, b) => Number(a[1]) - Number(b[1]));
|
||||
case 'value.ascending.as.version':
|
||||
return entries.sort((a, b) => compareVersionStrings(a[1], b[1]));
|
||||
case 'value.descending':
|
||||
case 'value.descending.as.string':
|
||||
return entries.sort((a, b) => b[1].localeCompare(a[1]));
|
||||
case 'value.descending.as.number':
|
||||
return entries.sort((a, b) => Number(b[1]) - Number(a[1]));
|
||||
case 'value.descending.as.version':
|
||||
return entries.sort((a, b) => compareVersionStrings(b[1], a[1]));
|
||||
default:
|
||||
throw new Error(`Unknown sorting pattern: ${pattern}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[] | SetIterator<string> | Set<string>} src
|
||||
* @param {ListSortable['sort'] | null} [pattern=null]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function sortByList(src, pattern = null) {
|
||||
const values = src instanceof Set ? Array.from(src.values()) : Array.from(src);
|
||||
switch (pattern) {
|
||||
case null:
|
||||
return values;
|
||||
case 'ascending':
|
||||
case 'ascending.as.string':
|
||||
return values.sort((a, b) => a.localeCompare(b));
|
||||
case 'ascending.as.number':
|
||||
return values.sort((a, b) => Number(a) - Number(b));
|
||||
case 'ascending.as.version':
|
||||
return values.sort((a, b) => compareVersionStrings(a, b));
|
||||
case 'descending':
|
||||
case 'descending.as.string':
|
||||
return values.sort((a, b) => b.localeCompare(a));
|
||||
case 'descending.as.number':
|
||||
return values.sort((a, b) => Number(b) - Number(a));
|
||||
case 'descending.as.version':
|
||||
return values.sort((a, b) => compareVersionStrings(b, a));
|
||||
default:
|
||||
throw new Error(`Unknown sorting pattern: ${pattern}`);
|
||||
}
|
||||
}
|
||||
125
.utils/utils/update-helper.mjs
Normal file
125
.utils/utils/update-helper.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
// utils/update-helper.mjs
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { parseProperties, readPropertiesSync, writePropertiesSyncWithLines, writePropertiesSyncWithMap } from './properties.mjs';
|
||||
import { printListDiffs, printMapDiffs } from './print.mjs';
|
||||
import { sortByList, sortByMap } from './sorting.mjs';
|
||||
import { generatePropertiesFileTimestamp } from './date.mjs';
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {Map<string, string>} map
|
||||
* @param {GradleMapUpdateOptions} [options={}]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function updateGradleMapData(filename, map, options = {}) {
|
||||
const niceName = filename.endsWith('.properties') ? filename : `${filename}.properties`;
|
||||
const filePath = path.resolve(process.cwd(), `../gradle/data/${niceName}`);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
const rwOptions = {
|
||||
encoding: options.encoding || 'utf8',
|
||||
sort: options.sort,
|
||||
};
|
||||
const original = readPropertiesSync(filePath, rwOptions);
|
||||
const updated = new Map(sortByMap(map.entries(), options.sort));
|
||||
if (!shallowEqualMaps(original, updated)) {
|
||||
writePropertiesSyncWithMap(filePath, updated, rwOptions);
|
||||
console.log(`[${niceName}] Updated` + (options.label ? ` (${options.label})` : ''));
|
||||
printMapDiffs(original, updated);
|
||||
} else {
|
||||
// console.log(`[${filename}] No update needed` + (options.label ? ` (${options.label})` : ''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {Set<string>} list
|
||||
* @param {GradleListUpdateOptions} [options={}]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function updateGradleListData(filename, list, options = {}) {
|
||||
const niceName = filename.endsWith('.list') ? filename : `${filename}.list`;
|
||||
const filePath = path.resolve(process.cwd(), `../gradle/data/${niceName}`);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
const rwOptions = {
|
||||
encoding: options.encoding || 'utf8',
|
||||
sort: options.sort,
|
||||
};
|
||||
const original = fs.readFileSync(filePath, { encoding: rwOptions.encoding })
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
const niceList = Array.from(list)
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
const updated = sortByList(niceList, options.sort);
|
||||
if (!shallowEqualLists(original, updated)) {
|
||||
fs.writeFileSync(filePath, generatePropertiesFileTimestamp() + '\n' + updated.join('\n') + '\n', { encoding: rwOptions.encoding });
|
||||
console.log(`[${niceName}] Updated` + (options.label ? ` (${options.label})` : ''));
|
||||
printListDiffs(original, updated);
|
||||
} else {
|
||||
// console.log(`[${filename}] No update needed` + (options.label ? ` (${options.label})` : ''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {string[]} lines
|
||||
* @param {GradleLinesUpdateOptions} [options={}]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function updateGradleLinesData(filename, lines, options = {}) {
|
||||
const niceName = filename.endsWith('.properties') ? filename : `${filename}.properties`;
|
||||
const filePath = path.resolve(process.cwd(), `../gradle/data/${niceName}`);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
const rwOptions = {
|
||||
encoding: options.encoding || 'utf8',
|
||||
};
|
||||
const original = readPropertiesSync(filePath, rwOptions);
|
||||
const linesToCheck = parseProperties(lines.join('\n'));
|
||||
if (!shallowEqualMaps(original, linesToCheck)) {
|
||||
writePropertiesSyncWithLines(filePath, lines, rwOptions);
|
||||
console.log(`[${niceName}] Updated` + (options.label ? ` (${options.label})` : ''));
|
||||
printMapDiffs(original, linesToCheck);
|
||||
} else {
|
||||
// console.log(`[${filename}] No update needed` + (options.label ? ` (${options.label})` : ''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string, string>} a
|
||||
* @param {Map<string, string>} b
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shallowEqualMaps(a, b) {
|
||||
if (a === b) return true;
|
||||
if (!(a instanceof Map) || !(b instanceof Map)) return false;
|
||||
if (a.size !== b.size) return false;
|
||||
for (const [ k, v ] of a) {
|
||||
if (!b.has(k)) return false;
|
||||
if (b.get(k) !== v) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} a
|
||||
* @param {string[]} b
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shallowEqualLists(a, b) {
|
||||
if (a === b) return true;
|
||||
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user