🛠️ Update various documentation, scripts, and configuration templates to enhance clarity, functionality, and maintainability across the project

This commit is contained in:
2026-05-04 10:56:41 +00:00
parent 1a2f1510bf
commit 31975e3088
41 changed files with 4184 additions and 133 deletions

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { isDeepStrictEqual } from 'node:util';
import fs from "node:fs";
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
function usage() {
console.error(
'Usage: node install/merge-vscode-settings.mjs --target <settings.json> --template <settings.template.jsonc> [--set NAME=value]'
"Usage: node install/merge-vscode-settings.mjs --target <settings.json> --template <settings.template.jsonc> [--set NAME=value]",
);
}
@@ -18,21 +18,21 @@ function parseArgs(argv) {
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === '--target') {
if (arg === "--target") {
options.target = argv[index + 1];
index += 1;
continue;
}
if (arg === '--template') {
if (arg === "--template") {
options.template = argv[index + 1];
index += 1;
continue;
}
if (arg === '--set') {
const assignment = argv[index + 1] ?? '';
const equalsIndex = assignment.indexOf('=');
if (arg === "--set") {
const assignment = argv[index + 1] ?? "";
const equalsIndex = assignment.indexOf("=");
if (equalsIndex <= 0) {
throw new Error(`Invalid --set assignment: ${assignment}`);
}
@@ -48,14 +48,14 @@ function parseArgs(argv) {
if (!options.target || !options.template) {
usage();
throw new Error('Both --target and --template are required.');
throw new Error("Both --target and --template are required.");
}
return options;
}
function stripJsonComments(input) {
let output = '';
let output = "";
let inString = false;
let escaping = false;
let inLineComment = false;
@@ -66,7 +66,7 @@ function stripJsonComments(input) {
const next = input[index + 1];
if (inLineComment) {
if (char === '\n') {
if (char === "\n") {
inLineComment = false;
output += char;
}
@@ -74,10 +74,10 @@ function stripJsonComments(input) {
}
if (inBlockComment) {
if (char === '*' && next === '/') {
if (char === "*" && next === "/") {
inBlockComment = false;
index += 1;
} else if (char === '\n' || char === '\r') {
} else if (char === "\n" || char === "\r") {
output += char;
}
continue;
@@ -87,7 +87,7 @@ function stripJsonComments(input) {
output += char;
if (escaping) {
escaping = false;
} else if (char === '\\') {
} else if (char === "\\") {
escaping = true;
} else if (char === '"') {
inString = false;
@@ -101,13 +101,13 @@ function stripJsonComments(input) {
continue;
}
if (char === '/' && next === '/') {
if (char === "/" && next === "/") {
inLineComment = true;
index += 1;
continue;
}
if (char === '/' && next === '*') {
if (char === "/" && next === "*") {
inBlockComment = true;
index += 1;
continue;
@@ -120,7 +120,7 @@ function stripJsonComments(input) {
}
function stripTrailingCommas(input) {
let output = '';
let output = "";
let inString = false;
let escaping = false;
@@ -131,7 +131,7 @@ function stripTrailingCommas(input) {
output += char;
if (escaping) {
escaping = false;
} else if (char === '\\') {
} else if (char === "\\") {
escaping = true;
} else if (char === '"') {
inString = false;
@@ -145,12 +145,12 @@ function stripTrailingCommas(input) {
continue;
}
if (char === ',') {
if (char === ",") {
let lookahead = index + 1;
while (lookahead < input.length && /\s/.test(input[lookahead])) {
lookahead += 1;
}
if (input[lookahead] === '}' || input[lookahead] === ']') {
if (input[lookahead] === "}" || input[lookahead] === "]") {
continue;
}
}
@@ -166,9 +166,7 @@ function renderTemplate(input, replacements) {
if (!(key in replacements)) {
return match;
}
return replacements[key]
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"');
return replacements[key].replace(/\\/g, "\\\\").replace(/"/g, '\\"');
});
}
@@ -209,21 +207,24 @@ function parseJsoncAst(input, label) {
continue;
}
if (char === '/' && next === '/') {
if (char === "/" && next === "/") {
index += 2;
while (index < input.length && input[index] !== '\n') {
while (index < input.length && input[index] !== "\n") {
index += 1;
}
continue;
}
if (char === '/' && next === '*') {
if (char === "/" && next === "*") {
index += 2;
while (index < input.length && !(input[index] === '*' && input[index + 1] === '/')) {
while (
index < input.length &&
!(input[index] === "*" && input[index + 1] === "/")
) {
index += 1;
}
if (index >= input.length) {
fail('unterminated block comment');
fail("unterminated block comment");
}
index += 2;
continue;
@@ -240,7 +241,7 @@ function parseJsoncAst(input, label) {
while (index < input.length) {
const char = input[index];
if (char === '\\') {
if (char === "\\") {
index += 2;
continue;
}
@@ -249,7 +250,7 @@ function parseJsoncAst(input, label) {
index += 1;
const raw = input.slice(start, index);
return {
type: 'string',
type: "string",
start,
end: index,
value: JSON.parse(raw),
@@ -259,7 +260,7 @@ function parseJsoncAst(input, label) {
index += 1;
}
fail('unterminated string literal');
fail("unterminated string literal");
}
function parseLiteralNode(expectedText, value) {
@@ -277,7 +278,9 @@ function parseJsoncAst(input, label) {
}
function parseNumberNode() {
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(input.slice(index));
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(
input.slice(index),
);
if (!match) {
fail(`invalid number at offset ${index}`);
}
@@ -285,7 +288,7 @@ function parseJsoncAst(input, label) {
const start = index;
index += match[0].length;
return {
type: 'number',
type: "number",
start,
end: index,
value: Number(match[0]),
@@ -298,29 +301,29 @@ function parseJsoncAst(input, label) {
const elements = [];
skipTrivia();
while (index < input.length && input[index] !== ']') {
while (index < input.length && input[index] !== "]") {
const valueNode = parseValueNode();
elements.push(valueNode);
skipTrivia();
if (input[index] === ',') {
if (input[index] === ",") {
index += 1;
skipTrivia();
continue;
}
if (input[index] !== ']') {
if (input[index] !== "]") {
fail(`expected ',' or ']' at offset ${index}`);
}
}
if (input[index] !== ']') {
fail('unterminated array');
if (input[index] !== "]") {
fail("unterminated array");
}
index += 1;
return {
type: 'array',
type: "array",
start,
end: index,
elements,
@@ -335,7 +338,7 @@ function parseJsoncAst(input, label) {
const value = {};
skipTrivia();
while (index < input.length && input[index] !== '}') {
while (index < input.length && input[index] !== "}") {
if (input[index] !== '"') {
fail(`expected string property name at offset ${index}`);
}
@@ -344,7 +347,7 @@ function parseJsoncAst(input, label) {
const key = keyNode.value;
skipTrivia();
if (input[index] !== ':') {
if (input[index] !== ":") {
fail(`expected ':' after property name at offset ${index}`);
}
@@ -363,25 +366,25 @@ function parseJsoncAst(input, label) {
value[key] = valueNode.value;
skipTrivia();
if (input[index] === ',') {
if (input[index] === ",") {
property.hasTrailingComma = true;
index += 1;
skipTrivia();
continue;
}
if (input[index] !== '}') {
if (input[index] !== "}") {
fail(`expected ',' or '}' at offset ${index}`);
}
}
if (input[index] !== '}') {
fail('unterminated object');
if (input[index] !== "}") {
fail("unterminated object");
}
index += 1;
return {
type: 'object',
type: "object",
start,
end: index,
properties,
@@ -393,25 +396,25 @@ function parseJsoncAst(input, label) {
skipTrivia();
const char = input[index];
if (char === '{') {
if (char === "{") {
return parseObjectNode();
}
if (char === '[') {
if (char === "[") {
return parseArrayNode();
}
if (char === '"') {
return parseStringNode();
}
if (char === 't') {
return parseLiteralNode('true', true);
if (char === "t") {
return parseLiteralNode("true", true);
}
if (char === 'f') {
return parseLiteralNode('false', false);
if (char === "f") {
return parseLiteralNode("false", false);
}
if (char === 'n') {
return parseLiteralNode('null', null);
if (char === "n") {
return parseLiteralNode("null", null);
}
if (char === '-' || /\d/.test(char ?? '')) {
if (char === "-" || /\d/.test(char ?? "")) {
return parseNumberNode();
}
@@ -426,7 +429,7 @@ function parseJsoncAst(input, label) {
fail(`unexpected trailing content at offset ${index}`);
}
if (root.type !== 'object') {
if (root.type !== "object") {
throw new Error(`${label} must contain a JSON object at the root.`);
}
@@ -434,31 +437,31 @@ function parseJsoncAst(input, label) {
}
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function detectIndentUnit(input) {
const matches = input.match(/^( +|\t+)\S/m);
if (!matches) {
return ' ';
return " ";
}
return matches[1];
}
function detectEol(input) {
return input.includes('\r\n') ? '\r\n' : '\n';
return input.includes("\r\n") ? "\r\n" : "\n";
}
function lineStartIndex(input, index) {
const newlineIndex = input.lastIndexOf('\n', index - 1);
const newlineIndex = input.lastIndexOf("\n", index - 1);
return newlineIndex === -1 ? 0 : newlineIndex + 1;
}
function lineIndentAt(input, index) {
const start = lineStartIndex(input, index);
let end = start;
while (end < input.length && (input[end] === ' ' || input[end] === '\t')) {
while (end < input.length && (input[end] === " " || input[end] === "\t")) {
end += 1;
}
return input.slice(start, end);
@@ -466,12 +469,12 @@ function lineIndentAt(input, index) {
function renderValue(value, propertyIndent, indentUnit, eol) {
const raw = JSON.stringify(value, null, indentUnit);
if (!raw.includes('\n')) {
if (!raw.includes("\n")) {
return raw;
}
return raw
.split('\n')
.split("\n")
.map((line, index) => (index === 0 ? line : `${propertyIndent}${line}`))
.join(eol);
}
@@ -488,19 +491,35 @@ function getObjectChildIndent(input, objectNode, indentUnit) {
return `${lineIndentAt(input, objectNode.start)}${indentUnit}`;
}
function buildMergeEdits(input, objectNode, managedSettings, indentUnit, eol, edits) {
function buildMergeEdits(
input,
objectNode,
managedSettings,
indentUnit,
eol,
edits,
) {
const missingEntries = [];
for (const [key, managedValue] of Object.entries(managedSettings)) {
const property = objectNode.properties.find((candidate) => candidate.key === key);
const property = objectNode.properties.find(
(candidate) => candidate.key === key,
);
if (!property) {
missingEntries.push([key, managedValue]);
continue;
}
if (isPlainObject(managedValue) && property.value.type === 'object') {
buildMergeEdits(input, property.value, managedValue, indentUnit, eol, edits);
if (isPlainObject(managedValue) && property.value.type === "object") {
buildMergeEdits(
input,
property.value,
managedValue,
indentUnit,
eol,
edits,
);
continue;
}
@@ -520,14 +539,18 @@ function buildMergeEdits(input, objectNode, managedSettings, indentUnit, eol, ed
const propertyIndent = getObjectChildIndent(input, objectNode, indentUnit);
const closingIndent = lineIndentAt(input, objectNode.end);
const closingBraceIndex = objectNode.end - 1;
const renderedProperties = missingEntries
.map(([key, value]) => `${propertyIndent}${renderProperty(key, value, propertyIndent, indentUnit, eol)}`)
.map(
([key, value]) =>
`${propertyIndent}${renderProperty(key, value, propertyIndent, indentUnit, eol)}`,
)
.join(`,${eol}`);
if (objectNode.properties.length === 0) {
edits.push({
start: objectNode.start + 1,
end: objectNode.end,
end: closingBraceIndex,
text: `${eol}${renderedProperties}${eol}${closingIndent}`,
});
return;
@@ -538,7 +561,7 @@ function buildMergeEdits(input, objectNode, managedSettings, indentUnit, eol, ed
edits.push({
start: lastProperty.value.end,
end: lastProperty.value.end,
text: ',',
text: ",",
});
}
@@ -558,27 +581,36 @@ function applyEdits(input, edits) {
return edits
.sort((left, right) => right.start - left.start || right.end - left.end)
.reduce(
(text, edit) => `${text.slice(0, edit.start)}${edit.text}${text.slice(edit.end)}`,
input
(text, edit) =>
`${text.slice(0, edit.start)}${edit.text}${text.slice(edit.end)}`,
input,
);
}
function main() {
const options = parseArgs(process.argv.slice(2));
const templateText = fs.readFileSync(options.template, 'utf8');
const templateText = fs.readFileSync(options.template, "utf8");
const renderedTemplate = renderTemplate(templateText, options.replacements);
const managedSettings = parseJsonc(renderedTemplate, options.template);
const targetText = fs.existsSync(options.target)
? fs.readFileSync(options.target, 'utf8')
: '{}\n';
? fs.readFileSync(options.target, "utf8")
: "{}\n";
const targetAst = parseJsoncAst(targetText, options.target);
const indentUnit = detectIndentUnit(targetText);
const eol = detectEol(targetText);
const edits = [];
buildMergeEdits(targetText, targetAst, managedSettings, indentUnit, eol, edits);
const output = edits.length === 0 ? targetText : applyEdits(targetText, edits);
buildMergeEdits(
targetText,
targetAst,
managedSettings,
indentUnit,
eol,
edits,
);
const output =
edits.length === 0 ? targetText : applyEdits(targetText, edits);
parseJsonc(output, options.target);
@@ -589,8 +621,8 @@ function main() {
return;
}
fs.writeFileSync(options.target, output, 'utf8');
fs.writeFileSync(options.target, output, "utf8");
console.log(`Merged managed VS Code settings into: ${options.target}`);
}
main();
main();