mirror of https://github.com/theNewDynamic/gohugo-theme-ananke.git

Patrick Kollitsch
yesterday 3d7b46278db46d25cfa086478df22dbfb3ae8cf9
build(deps): update dependencies and fix migration issues
6 files modified
5123 ■■■■■ changed files
.github/workflows/quickstart.yml 4 ●●●●● patch | view | raw | blame | history
.gitignore 6 ●●●●● patch | view | raw | blame | history
package-lock.json 3699 ●●●●● patch | view | raw | blame | history
package.json 180 ●●●●● patch | view | raw | blame | history
scripts/synch-social-icons.ts 1232 ●●●●● patch | view | raw | blame | history
tests/e2e/accessibility.spec.ts 2 ●●●●● patch | view | raw | blame | history
.github/workflows/quickstart.yml
@@ -20,13 +20,13 @@
    steps:
      - name: Check out repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          persist-credentials: false
          submodules: false
      - name: Set up Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
        uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
        with:
          node-version: "24"
.gitignore
@@ -5,4 +5,8 @@
todo*.md
# wireit
.wireit
.wireit
# Local project-management working files
/PROJECT.md
/TODO.md
package-lock.json
Diff too large
package.json
@@ -1,88 +1,96 @@
{
  "name": "@gohugo-ananke/ananke",
  "description": "Ananke: A theme for Hugo Sites",
  "version": "2.19.0",
  "license": "MIT",
  "repository": "gohugo-ananke/ananke",
  "author": "Bud Parr (https://github.com/budparr)",
  "maintainers": [
    {
      "name": "Patrick Kollitsch",
      "url": "https://github.com/davidsneighbour"
    }
  ],
  "contributors": [
    {
      "name": "Regis Philibert",
      "url": "https://github.com/regisphilibert"
    }
  ],
  "bugs": "https://github.com/gohugo-ananke/ananke/issues",
  "homepage": "https://github.com/gohugo-ananke/ananke#readme",
  "keywords": [
    "hugo",
    "hugo-theme",
    "hugo-module"
  ],
  "dependencies": {
    "cssnano": "8.0.1",
    "postcss": "8.5.15",
    "postcss-preset-env": "11.3.0",
    "tachyons": "4.12.0"
  },
  "devDependencies": {
    "@axe-core/playwright": "4.10.2",
    "@biomejs/biome": "2.4.16",
    "@github/markdownlint-github": "0.8.0",
    "@playwright/test": "1.60.0",
    "@release-it/conventional-changelog": "11.0.0",
    "@types/node": "25.9.1",
    "dotenv": "17.4.2",
    "lint-staged": "17.0.7",
    "lockfile-lint": "5.0.0",
    "markdownlint": "0.40.0",
    "markdownlint-cli2": "0.22.1",
    "markdownlint-rule-extended-ascii": "0.2.1",
    "markdownlint-rule-list-duplicates": "0.0.1",
    "markdownlint-rule-relative-links": "5.1.0",
    "markdownlint-rule-search-replace": "1.2.0",
    "markdownlint-rule-title-case-style": "0.4.3",
    "release-it": "20.2.0",
    "simple-git-hooks": "2.13.1",
    "typescript": "6.0.3",
    "yaml": "2.7.0"
  },
  "scripts": {
    "hook:commit": "lint-staged --config .lintstagedrc.js",
    "hook:push": "node -e \"process.exit(require('child_process').execSync('git branch --show-current').toString().trim() === 'main' ? 0 : 1)\" && node scripts/test-hugo-quickstart.ts || true",
    "lint:links": "lychee --config lychee.toml public",
    "lint:markdown": "markdownlint-cli2",
    "lint:markdown:fix": "markdownlint-cli2 --fix",
    "lint:typescript": "tsc -p tsconfig.json --noEmit",
    "prepare": "simple-git-hooks install",
    "release": "release-it --config .release-it.ts",
    "release:pre": "release-it --preRelease=prerelease --config .release-it.ts",
    "server": "hugo server --environment documentation",
    "test": "node scripts/test-hugo-quickstart.ts",
    "test:coverage-gate": "node tests/coverage-gate.mjs",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:quickstart": "node scripts/test-hugo-quickstart.ts",
    "test:quickstart:submodule": "node scripts/test-hugo-quickstart.ts --use-submodule",
    "update:docs": "git add docs/ && (git diff --cached --quiet || git commit -m \"chore(git): update documentation submodule\")"
  },
  "cspell": {
    "version": "0.2",
    "language": "en-GB",
    "ignorePaths": [
      "CHANGELOG.md",
      "public",
      "node_modules"
    ]
  },
  "simple-git-hooks": {
    "pre-commit": "npm run hook:commit",
    "pre-push": "npm run hook:push"
  },
  "type": "module"
    "name": "@gohugo-ananke/ananke",
    "description": "Ananke: A theme for Hugo Sites",
    "version": "2.19.0",
    "license": "MIT",
    "repository": "gohugo-ananke/ananke",
    "author": "Bud Parr (https://github.com/budparr)",
    "maintainers": [
        {
            "name": "Patrick Kollitsch",
            "url": "https://github.com/davidsneighbour"
        }
    ],
    "contributors": [
        {
            "name": "Regis Philibert",
            "url": "https://github.com/regisphilibert"
        }
    ],
    "bugs": "https://github.com/gohugo-ananke/ananke/issues",
    "homepage": "https://github.com/gohugo-ananke/ananke#readme",
    "keywords": [
        "hugo",
        "hugo-theme",
        "hugo-module"
    ],
    "dependencies": {
        "cssnano": "9.0.5",
        "postcss": "8.5.28",
        "postcss-preset-env": "11.5.4",
        "tachyons": "4.12.0"
    },
    "devDependencies": {
        "@axe-core/playwright": "4.13.0",
        "@biomejs/biome": "2.5.14",
        "@github/markdownlint-github": "0.8.4",
        "@playwright/test": "1.63.0",
        "@release-it/conventional-changelog": "12.0.2",
        "@types/node": "26.6.3",
        "dotenv": "18.0.4",
        "lint-staged": "17.6.0",
        "lockfile-lint": "5.0.1",
        "markdownlint": "0.41.1",
        "markdownlint-cli2": "0.23.3",
        "markdownlint-rule-extended-ascii": "0.2.1",
        "markdownlint-rule-list-duplicates": "0.0.1",
        "markdownlint-rule-relative-links": "5.1.3",
        "markdownlint-rule-search-replace": "1.2.0",
        "markdownlint-rule-title-case-style": "0.4.3",
        "release-it": "21.1.0",
        "simple-git-hooks": "2.14.0",
        "typescript": "7.0.2",
        "yaml": "2.9.1"
    },
    "scripts": {
        "hook:commit": "lint-staged --config .lintstagedrc.js",
        "hook:push": "node -e \"process.exit(require('child_process').execSync('git branch --show-current').toString().trim() === 'main' ? 0 : 1)\" && node scripts/test-hugo-quickstart.ts || true",
        "lint:links": "lychee --config lychee.toml public",
        "lint:markdown": "markdownlint-cli2",
        "lint:markdown:fix": "markdownlint-cli2 --fix",
        "lint:typescript": "tsc -p tsconfig.json --noEmit",
        "prepare": "simple-git-hooks install",
        "release": "release-it --config .release-it.ts",
        "release:pre": "release-it --preRelease=prerelease --config .release-it.ts",
        "server": "hugo server --environment documentation",
        "test": "node scripts/test-hugo-quickstart.ts",
        "test:coverage-gate": "node tests/coverage-gate.mjs",
        "test:e2e": "playwright test",
        "test:e2e:ui": "playwright test --ui",
        "test:quickstart": "node scripts/test-hugo-quickstart.ts",
        "test:quickstart:submodule": "node scripts/test-hugo-quickstart.ts --use-submodule",
        "update:docs": "git add docs/ && (git diff --cached --quiet || git commit -m \"chore(git): update documentation submodule\")"
    },
    "cspell": {
        "version": "0.2",
        "language": "en-GB",
        "ignorePaths": [
            "CHANGELOG.md",
            "public",
            "node_modules"
        ]
    },
    "simple-git-hooks": {
        "pre-commit": "npm run hook:commit",
        "pre-push": "npm run hook:push"
    },
    "type": "module",
    "overrides": {
        "markdownlint-rule-title-case-style": {
            "markdownlint": "$markdownlint"
        }
    },
    "allowScripts": {
        "simple-git-hooks@2.14.0": true
    }
}
scripts/synch-social-icons.ts
@@ -27,86 +27,86 @@
 * - node src/scripts/update-social-icons.ts --json
 */
import { promises as fs } from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { promises as fs } from "node:fs";
import path from "node:path";
import process from "node:process";
type CliOptions = {
    dir: string;
    branch: string;
    apply: boolean;
    deleteMissing: boolean;
    onlyChanged: boolean;
    json: boolean;
    verbose: boolean;
    all: boolean;
    concurrency: number;
    help: boolean;
    dir: string;
    branch: string;
    apply: boolean;
    deleteMissing: boolean;
    onlyChanged: boolean;
    json: boolean;
    verbose: boolean;
    all: boolean;
    concurrency: number;
    help: boolean;
};
type FileResultStatus =
    | 'same'
    | 'changed'
    | 'missing'
    | 'would-create'
    | 'created'
    | 'would-delete'
    | 'deleted'
    | 'updated'
    | 'error';
    | "same"
    | "changed"
    | "missing"
    | "would-create"
    | "created"
    | "would-delete"
    | "deleted"
    | "updated"
    | "error";
type FileResult = {
    fileName: string;
    localPath: string;
    remoteUrl: string;
    status: FileResultStatus;
    message: string;
    fileName: string;
    localPath: string;
    remoteUrl: string;
    status: FileResultStatus;
    message: string;
};
type Summary = {
    same: number;
    changed: number;
    missing: number;
    wouldCreate: number;
    created: number;
    wouldDelete: number;
    deleted: number;
    updated: number;
    errors: number;
    same: number;
    changed: number;
    missing: number;
    wouldCreate: number;
    created: number;
    wouldDelete: number;
    deleted: number;
    updated: number;
    errors: number;
};
type JsonOutput = {
    options: {
        dir: string;
        branch: string;
        apply: boolean;
        deleteMissing: boolean;
        onlyChanged: boolean;
        json: boolean;
        verbose: boolean;
        all: boolean;
        concurrency: number;
    };
    results: FileResult[];
    summary: Summary;
    options: {
        dir: string;
        branch: string;
        apply: boolean;
        deleteMissing: boolean;
        onlyChanged: boolean;
        json: boolean;
        verbose: boolean;
        all: boolean;
        concurrency: number;
    };
    results: FileResult[];
    summary: Summary;
};
type ProcessingTask =
    | {
        kind: 'upstream';
        fileName: string;
    }
    | {
        kind: 'local-only';
        fileName: string;
    };
    | {
            kind: "upstream";
            fileName: string;
      }
    | {
            kind: "local-only";
            fileName: string;
      };
const DEFAULT_DIRECTORY = 'assets/ananke/socials';
const DEFAULT_BRANCH = 'develop';
const SIMPLE_ICONS_OWNER = 'simple-icons';
const SIMPLE_ICONS_REPO = 'simple-icons';
const DEFAULT_DIRECTORY = "assets/ananke/socials";
const DEFAULT_BRANCH = "develop";
const SIMPLE_ICONS_OWNER = "simple-icons";
const SIMPLE_ICONS_REPO = "simple-icons";
const SIMPLE_ICONS_RAW_BASE =
    'https://raw.githubusercontent.com/simple-icons/simple-icons';
    "https://raw.githubusercontent.com/simple-icons/simple-icons";
const EXIT_SUCCESS = 0;
const EXIT_FAILURE = 1;
@@ -115,9 +115,10 @@
 * Print CLI help.
 */
function printHelp(): void {
    const scriptName = path.basename(process.argv[1] ?? 'update-social-icons.ts');
    const scriptName = path.basename(process.argv[1] ?? "update-social-icons.ts");
    console.log(`
    console.log(
        `
Usage:
  ${scriptName} [options]
@@ -167,7 +168,8 @@
  * deleted       local file missing upstream and was deleted
  * updated       local file was replaced because --apply was used
  * error         processing failed
`.trim());
`.trim(),
    );
}
/**
@@ -177,118 +179,121 @@
 * @returns Parsed CLI options.
 */
function parseArgs(argv: string[]): CliOptions {
    const options: CliOptions = {
        dir: DEFAULT_DIRECTORY,
        branch: DEFAULT_BRANCH,
        apply: false,
        deleteMissing: false,
        onlyChanged: false,
        json: false,
        verbose: false,
        all: false,
        concurrency: 8,
        help: false,
    };
    const options: CliOptions = {
        dir: DEFAULT_DIRECTORY,
        branch: DEFAULT_BRANCH,
        apply: false,
        deleteMissing: false,
        onlyChanged: false,
        json: false,
        verbose: false,
        all: false,
        concurrency: 8,
        help: false,
    };
    for (let index = 0; index < argv.length; index += 1) {
        const arg = argv[index];
    for (let index = 0; index < argv.length; index += 1) {
        const arg = argv[index];
        if (arg === undefined) {
            continue;
        }
        if (arg === '--help') {
            options.help = true;
            continue;
        }
        if (arg === "--help") {
            options.help = true;
            continue;
        }
        if (arg === '--apply') {
            options.apply = true;
            continue;
        }
        if (arg === "--apply") {
            options.apply = true;
            continue;
        }
        if (arg === '--delete') {
            options.deleteMissing = true;
            continue;
        }
        if (arg === "--delete") {
            options.deleteMissing = true;
            continue;
        }
        if (arg === '--only-changed') {
            options.onlyChanged = true;
            continue;
        }
        if (arg === "--only-changed") {
            options.onlyChanged = true;
            continue;
        }
        if (arg === '--json') {
            options.json = true;
            continue;
        }
        if (arg === "--json") {
            options.json = true;
            continue;
        }
        if (arg === '--verbose') {
            options.verbose = true;
            continue;
        }
        if (arg === "--verbose") {
            options.verbose = true;
            continue;
        }
        if (arg === '--all') {
            options.all = true;
            continue;
        }
        if (arg === "--all") {
            options.all = true;
            continue;
        }
        if (arg === '--dir') {
            const value = argv[index + 1];
            if (!value) {
                throw new Error('Missing value for --dir');
            }
            options.dir = value;
            index += 1;
            continue;
        }
        if (arg === "--dir") {
            const value = argv[index + 1];
            if (!value) {
                throw new Error("Missing value for --dir");
            }
            options.dir = value;
            index += 1;
            continue;
        }
        if (arg.startsWith('--dir=')) {
            options.dir = arg.slice('--dir='.length);
            continue;
        }
        if (arg.startsWith("--dir=")) {
            options.dir = arg.slice("--dir=".length);
            continue;
        }
        if (arg === '--branch') {
            const value = argv[index + 1];
            if (!value) {
                throw new Error('Missing value for --branch');
            }
            options.branch = value;
            index += 1;
            continue;
        }
        if (arg === "--branch") {
            const value = argv[index + 1];
            if (!value) {
                throw new Error("Missing value for --branch");
            }
            options.branch = value;
            index += 1;
            continue;
        }
        if (arg.startsWith('--branch=')) {
            options.branch = arg.slice('--branch='.length);
            continue;
        }
        if (arg.startsWith("--branch=")) {
            options.branch = arg.slice("--branch=".length);
            continue;
        }
        if (arg === '--concurrency') {
            const value = argv[index + 1];
            if (!value) {
                throw new Error('Missing value for --concurrency');
            }
        if (arg === "--concurrency") {
            const value = argv[index + 1];
            if (!value) {
                throw new Error("Missing value for --concurrency");
            }
            const parsed = Number.parseInt(value, 10);
            if (!Number.isInteger(parsed) || parsed < 1) {
                throw new Error('--concurrency must be a positive integer');
            }
            const parsed = Number.parseInt(value, 10);
            if (!Number.isInteger(parsed) || parsed < 1) {
                throw new Error("--concurrency must be a positive integer");
            }
            options.concurrency = parsed;
            index += 1;
            continue;
        }
            options.concurrency = parsed;
            index += 1;
            continue;
        }
        if (arg.startsWith('--concurrency=')) {
            const value = arg.slice('--concurrency='.length);
            const parsed = Number.parseInt(value, 10);
            if (!Number.isInteger(parsed) || parsed < 1) {
                throw new Error('--concurrency must be a positive integer');
            }
        if (arg.startsWith("--concurrency=")) {
            const value = arg.slice("--concurrency=".length);
            const parsed = Number.parseInt(value, 10);
            if (!Number.isInteger(parsed) || parsed < 1) {
                throw new Error("--concurrency must be a positive integer");
            }
            options.concurrency = parsed;
            continue;
        }
            options.concurrency = parsed;
            continue;
        }
        throw new Error(`Unknown argument: ${arg}`);
    }
        throw new Error(`Unknown argument: ${arg}`);
    }
    return options;
    return options;
}
/**
@@ -299,7 +304,7 @@
 * @returns Raw GitHub URL.
 */
function buildRemoteUrl(branch: string, fileName: string): string {
    return `${SIMPLE_ICONS_RAW_BASE}/${encodeURIComponent(branch)}/icons/${encodeURIComponent(fileName)}`;
    return `${SIMPLE_ICONS_RAW_BASE}/${encodeURIComponent(branch)}/icons/${encodeURIComponent(fileName)}`;
}
/**
@@ -309,13 +314,13 @@
 * @returns Sorted list of SVG filenames.
 */
async function getLocalSvgFiles(directory: string): Promise<string[]> {
    const entries = await fs.readdir(directory, { withFileTypes: true });
    const entries = await fs.readdir(directory, { withFileTypes: true });
    return entries
        .filter((entry) => entry.isFile())
        .map((entry) => entry.name)
        .filter((fileName) => fileName.toLowerCase().endsWith('.svg'))
        .sort((left, right) => left.localeCompare(right));
    return entries
        .filter((entry) => entry.isFile())
        .map((entry) => entry.name)
        .filter((fileName) => fileName.toLowerCase().endsWith(".svg"))
        .sort((left, right) => left.localeCompare(right));
}
/**
@@ -325,50 +330,52 @@
 * @returns Sorted list of upstream SVG filenames.
 */
async function getUpstreamSvgFiles(branch: string): Promise<string[]> {
    const apiUrl = new URL(
        `https://api.github.com/repos/${encodeURIComponent(SIMPLE_ICONS_OWNER)}/${encodeURIComponent(SIMPLE_ICONS_REPO)}/contents/icons`,
    );
    apiUrl.searchParams.set('ref', branch);
    const apiUrl = new URL(
        `https://api.github.com/repos/${encodeURIComponent(SIMPLE_ICONS_OWNER)}/${encodeURIComponent(SIMPLE_ICONS_REPO)}/contents/icons`,
    );
    apiUrl.searchParams.set("ref", branch);
    const response = await fetch(apiUrl.toString(), {
        method: 'GET',
        headers: {
            Accept: 'application/vnd.github+json',
            'User-Agent': 'davidsneighbour-social-icon-updater',
        },
    });
    const response = await fetch(apiUrl.toString(), {
        method: "GET",
        headers: {
            Accept: "application/vnd.github+json",
            "User-Agent": "davidsneighbour-social-icon-updater",
        },
    });
    if (!response.ok) {
        throw new Error(
            `Failed to retrieve upstream icon listing with HTTP ${response.status}`,
        );
    }
    if (!response.ok) {
        throw new Error(
            `Failed to retrieve upstream icon listing with HTTP ${response.status}`,
        );
    }
    const data: unknown = await response.json();
    const data: unknown = await response.json();
    if (!Array.isArray(data)) {
        throw new Error('Unexpected response while retrieving upstream icon listing');
    }
    if (!Array.isArray(data)) {
        throw new Error(
            "Unexpected response while retrieving upstream icon listing",
        );
    }
    const fileNames = data
        .flatMap((entry: unknown) => {
            if (
                typeof entry === 'object' &&
                entry !== null &&
                'type' in entry &&
                'name' in entry &&
                entry.type === 'file' &&
                typeof entry.name === 'string' &&
                entry.name.toLowerCase().endsWith('.svg')
            ) {
                return [entry.name];
            }
    const fileNames = data
        .flatMap((entry: unknown) => {
            if (
                typeof entry === "object" &&
                entry !== null &&
                "type" in entry &&
                "name" in entry &&
                entry.type === "file" &&
                typeof entry.name === "string" &&
                entry.name.toLowerCase().endsWith(".svg")
            ) {
                return [entry.name];
            }
            return [];
        })
        .sort((left, right) => left.localeCompare(right));
            return [];
        })
        .sort((left, right) => left.localeCompare(right));
    return fileNames;
    return fileNames;
}
/**
@@ -380,23 +387,25 @@
 * @returns SVG content or null if not found.
 */
async function fetchRemoteSvg(remoteUrl: string): Promise<string | null> {
    const response = await fetch(remoteUrl, {
        method: 'GET',
        headers: {
            Accept: 'image/svg+xml,text/plain;q=0.9,*/*;q=0.8',
            'User-Agent': 'davidsneighbour-social-icon-updater',
        },
    });
    const response = await fetch(remoteUrl, {
        method: "GET",
        headers: {
            Accept: "image/svg+xml,text/plain;q=0.9,*/*;q=0.8",
            "User-Agent": "davidsneighbour-social-icon-updater",
        },
    });
    if (response.status === 404) {
        return null;
    }
    if (response.status === 404) {
        return null;
    }
    if (!response.ok) {
        throw new Error(`Request failed with HTTP ${response.status} for ${remoteUrl}`);
    }
    if (!response.ok) {
        throw new Error(
            `Request failed with HTTP ${response.status} for ${remoteUrl}`,
        );
    }
    return await response.text();
    return await response.text();
}
/**
@@ -418,94 +427,96 @@
 * @returns Processing result.
 */
async function processUpstreamFile(
    directory: string,
    fileName: string,
    branch: string,
    apply: boolean,
    directory: string,
    fileName: string,
    branch: string,
    apply: boolean,
): Promise<FileResult> {
    const localPath = path.join(directory, fileName);
    const remoteUrl = buildRemoteUrl(branch, fileName);
    const localPath = path.join(directory, fileName);
    const remoteUrl = buildRemoteUrl(branch, fileName);
    try {
        const remoteContent = await fetchRemoteSvg(remoteUrl);
    try {
        const remoteContent = await fetchRemoteSvg(remoteUrl);
        if (remoteContent === null) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: 'missing',
                message: 'No matching upstream file found',
            };
        }
        if (remoteContent === null) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: "missing",
                message: "No matching upstream file found",
            };
        }
        const localStats = await fs.stat(localPath).catch(() => null);
        const localStats = await fs.stat(localPath).catch(() => null);
        if (!localStats) {
            if (!apply) {
                return {
                    fileName,
                    localPath,
                    remoteUrl,
                    status: 'would-create',
                    message: 'Upstream file exists and would be created locally',
                };
            }
        if (!localStats) {
            if (!apply) {
                return {
                    fileName,
                    localPath,
                    remoteUrl,
                    status: "would-create",
                    message: "Upstream file exists and would be created locally",
                };
            }
            await fs.writeFile(localPath, remoteContent, 'utf8');
            await fs.writeFile(localPath, remoteContent, "utf8");
            return {
                fileName,
                localPath,
                remoteUrl,
                status: 'created',
                message: 'Upstream file existed and was created locally',
            };
        }
            return {
                fileName,
                localPath,
                remoteUrl,
                status: "created",
                message: "Upstream file existed and was created locally",
            };
        }
        const localContent = await fs.readFile(localPath, 'utf8');
        const localContent = await fs.readFile(localPath, "utf8");
        if (localContent === remoteContent) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: 'same',
                message: 'Local file matches upstream',
            };
        }
        if (localContent === remoteContent) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: "same",
                message: "Local file matches upstream",
            };
        }
        if (!apply) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: 'changed',
                message: 'Upstream file differs from local file',
            };
        }
        if (!apply) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: "changed",
                message: "Upstream file differs from local file",
            };
        }
        await fs.writeFile(localPath, remoteContent, 'utf8');
        await fs.writeFile(localPath, remoteContent, "utf8");
        return {
            fileName,
            localPath,
            remoteUrl,
            status: 'updated',
            message: 'Local file replaced with upstream version',
        };
    } catch (error: unknown) {
        const message =
            error instanceof Error ? error.message : 'Unknown error while processing file';
        return {
            fileName,
            localPath,
            remoteUrl,
            status: "updated",
            message: "Local file replaced with upstream version",
        };
    } catch (error: unknown) {
        const message =
            error instanceof Error
                ? error.message
                : "Unknown error while processing file";
        return {
            fileName,
            localPath,
            remoteUrl,
            status: 'error',
            message,
        };
    }
        return {
            fileName,
            localPath,
            remoteUrl,
            status: "error",
            message,
        };
    }
}
/**
@@ -519,57 +530,59 @@
 * @returns Processing result.
 */
async function processLocalOnlyFile(
    directory: string,
    fileName: string,
    branch: string,
    apply: boolean,
    deleteMissing: boolean,
    directory: string,
    fileName: string,
    branch: string,
    apply: boolean,
    deleteMissing: boolean,
): Promise<FileResult> {
    const localPath = path.join(directory, fileName);
    const remoteUrl = buildRemoteUrl(branch, fileName);
    const localPath = path.join(directory, fileName);
    const remoteUrl = buildRemoteUrl(branch, fileName);
    try {
        if (!deleteMissing) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: 'missing',
                message: 'No matching upstream file found',
            };
        }
    try {
        if (!deleteMissing) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: "missing",
                message: "No matching upstream file found",
            };
        }
        if (!apply) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: 'would-delete',
                message: 'No matching upstream file found; local file would be deleted',
            };
        }
        if (!apply) {
            return {
                fileName,
                localPath,
                remoteUrl,
                status: "would-delete",
                message: "No matching upstream file found; local file would be deleted",
            };
        }
        await fs.unlink(localPath);
        await fs.unlink(localPath);
        return {
            fileName,
            localPath,
            remoteUrl,
            status: 'deleted',
            message: 'No matching upstream file found; local file deleted',
        };
    } catch (error: unknown) {
        const message =
            error instanceof Error ? error.message : 'Unknown error while processing file';
        return {
            fileName,
            localPath,
            remoteUrl,
            status: "deleted",
            message: "No matching upstream file found; local file deleted",
        };
    } catch (error: unknown) {
        const message =
            error instanceof Error
                ? error.message
                : "Unknown error while processing file";
        return {
            fileName,
            localPath,
            remoteUrl,
            status: 'error',
            message,
        };
    }
        return {
            fileName,
            localPath,
            remoteUrl,
            status: "error",
            message,
        };
    }
}
/**
@@ -580,14 +593,14 @@
 * @returns True if the result should be shown.
 */
function shouldPrintResult(
    status: FileResultStatus,
    onlyChanged: boolean,
    status: FileResultStatus,
    onlyChanged: boolean,
): boolean {
    if (!onlyChanged) {
        return true;
    }
    if (!onlyChanged) {
        return true;
    }
    return status !== 'same';
    return status !== "same";
}
/**
@@ -597,24 +610,26 @@
 * @param verbose Whether to include detail lines.
 */
function printResult(result: FileResult, verbose: boolean): void {
    const prefixByStatus: Record<FileResultStatus, string> = {
        same: 'SAME',
        changed: 'CHANGED',
        missing: 'MISS',
        'would-create': 'WOULD-CREATE',
        created: 'CREATED',
        'would-delete': 'WOULD-DELETE',
        deleted: 'DELETED',
        updated: 'UPDATED',
        error: 'ERR',
    };
    const prefixByStatus: Record<FileResultStatus, string> = {
        same: "SAME",
        changed: "CHANGED",
        missing: "MISS",
        "would-create": "WOULD-CREATE",
        created: "CREATED",
        "would-delete": "WOULD-DELETE",
        deleted: "DELETED",
        updated: "UPDATED",
        error: "ERR",
    };
    console.log(`${prefixByStatus[result.status]} ${result.fileName} - ${result.message}`);
    console.log(
        `${prefixByStatus[result.status]} ${result.fileName} - ${result.message}`,
    );
    if (verbose) {
        console.log(`  local:  ${result.localPath}`);
        console.log(`  remote: ${result.remoteUrl}`);
    }
    if (verbose) {
        console.log(`  local:  ${result.localPath}`);
        console.log(`  remote: ${result.remoteUrl}`);
    }
}
/**
@@ -624,17 +639,19 @@
 * @returns Summary counts.
 */
function buildSummary(results: FileResult[]): Summary {
    return {
        same: results.filter((entry) => entry.status === 'same').length,
        changed: results.filter((entry) => entry.status === 'changed').length,
        missing: results.filter((entry) => entry.status === 'missing').length,
        wouldCreate: results.filter((entry) => entry.status === 'would-create').length,
        created: results.filter((entry) => entry.status === 'created').length,
        wouldDelete: results.filter((entry) => entry.status === 'would-delete').length,
        deleted: results.filter((entry) => entry.status === 'deleted').length,
        updated: results.filter((entry) => entry.status === 'updated').length,
        errors: results.filter((entry) => entry.status === 'error').length,
    };
    return {
        same: results.filter((entry) => entry.status === "same").length,
        changed: results.filter((entry) => entry.status === "changed").length,
        missing: results.filter((entry) => entry.status === "missing").length,
        wouldCreate: results.filter((entry) => entry.status === "would-create")
            .length,
        created: results.filter((entry) => entry.status === "created").length,
        wouldDelete: results.filter((entry) => entry.status === "would-delete")
            .length,
        deleted: results.filter((entry) => entry.status === "deleted").length,
        updated: results.filter((entry) => entry.status === "updated").length,
        errors: results.filter((entry) => entry.status === "error").length,
    };
}
/**
@@ -643,17 +660,17 @@
 * @param summary Summary counts.
 */
function printSummary(summary: Summary): void {
    console.log('');
    console.log('Summary');
    console.log(`  same:          ${summary.same}`);
    console.log(`  changed:       ${summary.changed}`);
    console.log(`  updated:       ${summary.updated}`);
    console.log(`  missing:       ${summary.missing}`);
    console.log(`  would-create:  ${summary.wouldCreate}`);
    console.log(`  created:       ${summary.created}`);
    console.log(`  would-delete:  ${summary.wouldDelete}`);
    console.log(`  deleted:       ${summary.deleted}`);
    console.log(`  errors:        ${summary.errors}`);
    console.log("");
    console.log("Summary");
    console.log(`  same:          ${summary.same}`);
    console.log(`  changed:       ${summary.changed}`);
    console.log(`  updated:       ${summary.updated}`);
    console.log(`  missing:       ${summary.missing}`);
    console.log(`  would-create:  ${summary.wouldCreate}`);
    console.log(`  created:       ${summary.created}`);
    console.log(`  would-delete:  ${summary.wouldDelete}`);
    console.log(`  deleted:       ${summary.deleted}`);
    console.log(`  errors:        ${summary.errors}`);
}
/**
@@ -664,18 +681,20 @@
 * @param active Number of currently active tasks.
 */
function renderProgress(
    processed: number,
    total: number,
    active: number,
    processed: number,
    total: number,
    active: number,
): void {
    process.stdout.write(`\rProcessing ${processed}/${total} (active: ${active})`);
    process.stdout.write(
        `\rProcessing ${processed}/${total} (active: ${active})`,
    );
}
/**
 * Clear the progress line from the terminal.
 */
function clearProgressLine(): void {
    process.stdout.write('\r\x1b[K');
    process.stdout.write("\r\x1b[K");
}
/**
@@ -685,266 +704,271 @@
 * @returns Results in task order.
 */
async function runTasksWithConcurrency<TInput, TResult>(params: {
    items: TInput[];
    concurrency: number;
    worker: (item: TInput) => Promise<TResult>;
    onResult?: (
        result: TResult,
        processed: number,
        total: number,
        active: number,
    ) => void;
    items: TInput[];
    concurrency: number;
    worker: (item: TInput) => Promise<TResult>;
    onResult?: (
        result: TResult,
        processed: number,
        total: number,
        active: number,
    ) => void;
}): Promise<TResult[]> {
    const { items, concurrency, worker, onResult } = params;
    const { items, concurrency, worker, onResult } = params;
    const results = new Array<TResult>(items.length);
    let nextIndex = 0;
    let processed = 0;
    let active = 0;
    const results = new Array<TResult>(items.length);
    let nextIndex = 0;
    let processed = 0;
    let active = 0;
    async function runWorker(): Promise<void> {
        while (true) {
            const currentIndex = nextIndex;
            if (currentIndex >= items.length) {
                return;
            }
    async function runWorker(): Promise<void> {
        while (true) {
            const currentIndex = nextIndex;
            if (currentIndex >= items.length) {
                return;
            }
            nextIndex += 1;
            active += 1;
            nextIndex += 1;
            active += 1;
            try {
                const result = await worker(items[currentIndex]);
                results[currentIndex] = result;
            } finally {
                active -= 1;
                processed += 1;
            try {
                // Index is bounds-checked above; TInput itself may include undefined.
                const result = await worker(items[currentIndex] as TInput);
                results[currentIndex] = result;
            } finally {
                active -= 1;
                processed += 1;
                const result = results[currentIndex];
                if (result !== undefined && onResult) {
                    onResult(result, processed, items.length, active);
                }
            }
        }
    }
                const result = results[currentIndex];
                if (result !== undefined && onResult) {
                    onResult(result, processed, items.length, active);
                }
            }
        }
    }
    const workerCount = Math.min(concurrency, items.length);
    await Promise.all(
        Array.from({ length: workerCount }, async () => {
            await runWorker();
        }),
    );
    const workerCount = Math.min(concurrency, items.length);
    await Promise.all(
        Array.from({ length: workerCount }, async () => {
            await runWorker();
        }),
    );
    return results;
    return results;
}
/**
 * Main program entry point.
 */
async function main(): Promise<void> {
    const options = parseArgs(process.argv.slice(2));
    const options = parseArgs(process.argv.slice(2));
    if (options.help) {
        printHelp();
        process.exit(EXIT_SUCCESS);
    }
    if (options.help) {
        printHelp();
        process.exit(EXIT_SUCCESS);
    }
    const targetDirectory = path.resolve(options.dir);
    const targetDirectory = path.resolve(options.dir);
    const stats = await fs.stat(targetDirectory).catch(() => null);
    if (!stats || !stats.isDirectory()) {
        throw new Error(`Directory does not exist or is not a directory: ${targetDirectory}`);
    }
    const stats = await fs.stat(targetDirectory).catch(() => null);
    if (!stats || !stats.isDirectory()) {
        throw new Error(
            `Directory does not exist or is not a directory: ${targetDirectory}`,
        );
    }
    const localFiles = await getLocalSvgFiles(targetDirectory);
    const localFiles = await getLocalSvgFiles(targetDirectory);
    let upstreamFiles: string[] = [];
    if (options.all) {
        upstreamFiles = await getUpstreamSvgFiles(options.branch);
    }
    let upstreamFiles: string[] = [];
    if (options.all) {
        upstreamFiles = await getUpstreamSvgFiles(options.branch);
    }
    const emptySummary: Summary = {
        same: 0,
        changed: 0,
        missing: 0,
        wouldCreate: 0,
        created: 0,
        wouldDelete: 0,
        deleted: 0,
        updated: 0,
        errors: 0,
    };
    const emptySummary: Summary = {
        same: 0,
        changed: 0,
        missing: 0,
        wouldCreate: 0,
        created: 0,
        wouldDelete: 0,
        deleted: 0,
        updated: 0,
        errors: 0,
    };
    const tasks: ProcessingTask[] = [];
    const tasks: ProcessingTask[] = [];
    if (options.all) {
        const upstreamSet = new Set(upstreamFiles);
    if (options.all) {
        const upstreamSet = new Set(upstreamFiles);
        for (const fileName of upstreamFiles) {
            tasks.push({
                kind: 'upstream',
                fileName,
            });
        }
        for (const fileName of upstreamFiles) {
            tasks.push({
                kind: "upstream",
                fileName,
            });
        }
        for (const fileName of localFiles) {
            if (!upstreamSet.has(fileName)) {
                tasks.push({
                    kind: 'local-only',
                    fileName,
                });
            }
        }
    } else {
        if (localFiles.length === 0) {
            if (options.json) {
                const emptyOutput: JsonOutput = {
                    options: {
                        dir: targetDirectory,
                        branch: options.branch,
                        apply: options.apply,
                        deleteMissing: options.deleteMissing,
                        onlyChanged: options.onlyChanged,
                        json: options.json,
                        verbose: options.verbose,
                        all: options.all,
                        concurrency: options.concurrency,
                    },
                    results: [],
                    summary: emptySummary,
                };
                console.log(JSON.stringify(emptyOutput, null, 2));
            } else {
                console.log(`No .svg files found in ${targetDirectory}`);
            }
        for (const fileName of localFiles) {
            if (!upstreamSet.has(fileName)) {
                tasks.push({
                    kind: "local-only",
                    fileName,
                });
            }
        }
    } else {
        if (localFiles.length === 0) {
            if (options.json) {
                const emptyOutput: JsonOutput = {
                    options: {
                        dir: targetDirectory,
                        branch: options.branch,
                        apply: options.apply,
                        deleteMissing: options.deleteMissing,
                        onlyChanged: options.onlyChanged,
                        json: options.json,
                        verbose: options.verbose,
                        all: options.all,
                        concurrency: options.concurrency,
                    },
                    results: [],
                    summary: emptySummary,
                };
                console.log(JSON.stringify(emptyOutput, null, 2));
            } else {
                console.log(`No .svg files found in ${targetDirectory}`);
            }
            process.exit(EXIT_SUCCESS);
        }
            process.exit(EXIT_SUCCESS);
        }
        for (const fileName of localFiles) {
            tasks.push({
                kind: 'upstream',
                fileName,
            });
        }
    }
        for (const fileName of localFiles) {
            tasks.push({
                kind: "upstream",
                fileName,
            });
        }
    }
    if (!options.json) {
        if (options.all) {
            console.log(
                `Syncing against all upstream SVG file(s) from simple-icons/icons/ into ${targetDirectory}`,
            );
            console.log(`Found ${upstreamFiles.length} upstream SVG file(s).`);
            console.log(`Found ${localFiles.length} local SVG file(s).`);
        } else {
            console.log(`Checking ${localFiles.length} local SVG file(s) in ${targetDirectory}`);
        }
    if (!options.json) {
        if (options.all) {
            console.log(
                `Syncing against all upstream SVG file(s) from simple-icons/icons/ into ${targetDirectory}`,
            );
            console.log(`Found ${upstreamFiles.length} upstream SVG file(s).`);
            console.log(`Found ${localFiles.length} local SVG file(s).`);
        } else {
            console.log(
                `Checking ${localFiles.length} local SVG file(s) in ${targetDirectory}`,
            );
        }
        console.log(`Using simple-icons branch: ${options.branch}`);
        console.log(`Concurrency: ${options.concurrency}`);
        console.log(
            options.apply
                ? 'Apply mode enabled. Changed files will be written.'
                : 'Dry run mode enabled. No files will be written.',
        );
        console.log(`Using simple-icons branch: ${options.branch}`);
        console.log(`Concurrency: ${options.concurrency}`);
        console.log(
            options.apply
                ? "Apply mode enabled. Changed files will be written."
                : "Dry run mode enabled. No files will be written.",
        );
        if (options.deleteMissing) {
            console.log(
                options.apply
                    ? 'Delete mode enabled. Local files missing upstream will be deleted.'
                    : 'Delete mode enabled. Local files missing upstream will be reported as would-delete.',
            );
        }
        if (options.deleteMissing) {
            console.log(
                options.apply
                    ? "Delete mode enabled. Local files missing upstream will be deleted."
                    : "Delete mode enabled. Local files missing upstream will be reported as would-delete.",
            );
        }
        if (options.onlyChanged) {
            console.log('Only changed results will be shown.');
        }
        if (options.onlyChanged) {
            console.log("Only changed results will be shown.");
        }
        console.log('');
    }
        console.log("");
    }
    const results = await runTasksWithConcurrency<ProcessingTask, FileResult>({
        items: tasks,
        concurrency: options.concurrency,
        worker: async (task): Promise<FileResult> => {
            if (task.kind === 'upstream') {
                const result = await processUpstreamFile(
                    targetDirectory,
                    task.fileName,
                    options.branch,
                    options.apply,
                );
    const results = await runTasksWithConcurrency<ProcessingTask, FileResult>({
        items: tasks,
        concurrency: options.concurrency,
        worker: async (task): Promise<FileResult> => {
            if (task.kind === "upstream") {
                const result = await processUpstreamFile(
                    targetDirectory,
                    task.fileName,
                    options.branch,
                    options.apply,
                );
                if (result.status === 'missing' && !options.all) {
                    return await processLocalOnlyFile(
                        targetDirectory,
                        task.fileName,
                        options.branch,
                        options.apply,
                        options.deleteMissing,
                    );
                }
                if (result.status === "missing" && !options.all) {
                    return await processLocalOnlyFile(
                        targetDirectory,
                        task.fileName,
                        options.branch,
                        options.apply,
                        options.deleteMissing,
                    );
                }
                return result;
            }
                return result;
            }
            return await processLocalOnlyFile(
                targetDirectory,
                task.fileName,
                options.branch,
                options.apply,
                options.deleteMissing,
            );
        },
        onResult: (result, processed, total, active): void => {
            if (!options.json) {
                renderProgress(processed, total, active);
            return await processLocalOnlyFile(
                targetDirectory,
                task.fileName,
                options.branch,
                options.apply,
                options.deleteMissing,
            );
        },
        onResult: (result, processed, total, active): void => {
            if (!options.json) {
                renderProgress(processed, total, active);
                if (shouldPrintResult(result.status, options.onlyChanged)) {
                    clearProgressLine();
                    printResult(result, options.verbose);
                    renderProgress(processed, total, active);
                }
            }
        },
    });
                if (shouldPrintResult(result.status, options.onlyChanged)) {
                    clearProgressLine();
                    printResult(result, options.verbose);
                    renderProgress(processed, total, active);
                }
            }
        },
    });
    if (!options.json) {
        clearProgressLine();
    }
    if (!options.json) {
        clearProgressLine();
    }
    const summary = buildSummary(results);
    const summary = buildSummary(results);
    if (options.json) {
        const output: JsonOutput = {
            options: {
                dir: targetDirectory,
                branch: options.branch,
                apply: options.apply,
                deleteMissing: options.deleteMissing,
                onlyChanged: options.onlyChanged,
                json: options.json,
                verbose: options.verbose,
                all: options.all,
                concurrency: options.concurrency,
            },
            results: options.onlyChanged
                ? results.filter((result) => shouldPrintResult(result.status, true))
                : results,
            summary,
        };
    if (options.json) {
        const output: JsonOutput = {
            options: {
                dir: targetDirectory,
                branch: options.branch,
                apply: options.apply,
                deleteMissing: options.deleteMissing,
                onlyChanged: options.onlyChanged,
                json: options.json,
                verbose: options.verbose,
                all: options.all,
                concurrency: options.concurrency,
            },
            results: options.onlyChanged
                ? results.filter((result) => shouldPrintResult(result.status, true))
                : results,
            summary,
        };
        console.log(JSON.stringify(output, null, 2));
    } else {
        printSummary(summary);
    }
        console.log(JSON.stringify(output, null, 2));
    } else {
        printSummary(summary);
    }
    const hasErrors = results.some((entry) => entry.status === 'error');
    process.exit(hasErrors ? EXIT_FAILURE : EXIT_SUCCESS);
    const hasErrors = results.some((entry) => entry.status === "error");
    process.exit(hasErrors ? EXIT_FAILURE : EXIT_SUCCESS);
}
main().catch((error: unknown) => {
    const message =
        error instanceof Error ? error.message : 'Unknown fatal error';
    console.error(`Fatal error: ${message}`);
    process.exit(EXIT_FAILURE);
});
    const message =
        error instanceof Error ? error.message : "Unknown fatal error";
    console.error(`Fatal error: ${message}`);
    process.exit(EXIT_FAILURE);
});
tests/e2e/accessibility.spec.ts
@@ -1,4 +1,4 @@
import AxeBuilder from "@axe-core/playwright";
import { AxeBuilder } from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
/**