6.7.0 - Alpha6 - Scrapers 工具的更新机制由 "锚点更新" 替换为 "结构化更新"

This commit is contained in:
SuperMonster003
2025-10-02 13:11:54 +08:00
parent f3e04b9ca2
commit 0b01bba618
33 changed files with 1599 additions and 1565 deletions

View File

@@ -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 };
}

View File

@@ -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');
}

View File

@@ -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
View 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}`);
}
}

View 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;
}