#!/usr/bin/env node /** * Builds src/ui/settings/settings-search-index.ts: the i18n keys each core * settings section shows, so the Settings search can match on any label in * a section without mounting it. Run it after changing a settings section. * * --check fails when the committed file is out of date. */ const fs = require("node:fs"); const path = require("node:path"); const ROOT = path.resolve(__dirname, ".."); const SIDEBAR = path.join(ROOT, "src", "ui", "sidebar"); const OUT = path.join( ROOT, "src", "ui", "settings", "settings-search-index.ts", ); function keysIn(text) { return [ ...new Set([...text.matchAll(/\bt\(\s*"([\w.-]+)"/g)].map((m) => m[1])), ]; } function read(file) { return fs.readFileSync(path.join(SIDEBAR, file), "utf8"); } /** The body of one exported or local function, by name. */ function functionBody(source, name) { const start = source.search(new RegExp(`function ${name}\\b`)); if (start < 0) throw new Error(`No function ${name}`); const next = source.slice(start + 1).search(/\n(export )?function \w+/); return next < 0 ? source.slice(start) : source.slice(start, start + 1 + next); } /** Profile sections, split on their AccordionSection ids. */ function profileSections() { const source = read("UserProfilePanel.tsx"); const lines = source.split("\n"); const starts = []; lines.forEach((line, i) => { if (/^\s{6} { const end = k + 1 < starts.length ? starts[k + 1] : lines.length; const chunk = lines.slice(start, end).join("\n"); const id = (chunk.match(/\bid="([^"]+)"/) || [])[1]; if (id) out[id] = keysIn(chunk); }); out.interface = [ ...new Set([ ...(out.interface ?? []), ...keysIn(read("InterfacePresetSettings.tsx")), ]), ]; out.security = [ ...new Set([ ...(out.security ?? []), ...keysIn(read("AuthEnrollmentSections.tsx")), ...keysIn(functionBody(source, "PasswordChangeSection")), ]), ]; out["api-keys"] = [ ...new Set([ ...(out["api-keys"] ?? []), ...keysIn(functionBody(source, "NewApiKeyDialog")), ]), ]; out.keybindings = keysIn( fs.readFileSync( path.join(ROOT, "src", "ui", "settings", "KeybindingsSettings.tsx"), "utf8", ), ); return out; } function adminSections() { const settings = read("AdminSettingsSections.tsx"); const management = read("AdminManagementSections.tsx"); return { "admin-general": [ ...new Set([ ...keysIn(functionBody(settings, "AdminGeneralSettingsSection")), ...keysIn(read("AdminPluginDeveloperMode.tsx")), ]), ], "admin-users": [ ...new Set([ ...keysIn(functionBody(management, "AdminUsersSection")), ...keysIn(read("AdminUserDialogs.tsx")), ]), ], "admin-sessions": keysIn(functionBody(management, "AdminSessionsSection")), "admin-roles": keysIn(functionBody(management, "AdminRolesSection")), "admin-host-defaults": [ ...new Set([ ...keysIn(functionBody(settings, "AdminHostDefaultsSection")), ...keysIn(read("AdminHostTags.tsx")), ]), ], "admin-branding": keysIn(read("AdminBrandingSection.tsx")), "admin-database": keysIn(functionBody(settings, "AdminDatabaseSection")), "admin-ssl": keysIn(functionBody(settings, "AdminSSLSection")), "admin-api-keys": keysIn(read("AdminApiKeysSection.tsx")), "admin-audit-log": keysIn(read("AdminAuditLogSection.tsx")), updates: keysIn( fs.readFileSync( path.join(ROOT, "src", "ui", "settings", "UpdatesSettings.tsx"), "utf8", ), ), }; } function render() { const index = { ...profileSections(), ...adminSections() }; const body = Object.entries(index) .sort(([a], [b]) => a.localeCompare(b)) .map( ([id, keys]) => ` ${JSON.stringify(id)}: [\n${keys .sort() .map((key) => ` ${JSON.stringify(key)},`) .join("\n")}\n ],`, ) .join("\n"); return `// Generated by scripts/generate-settings-search-index.cjs. Do not edit.\n\n/** The i18n keys each core settings section shows, for search. */\nexport const SETTINGS_SEARCH_INDEX: Record = {\n${body}\n};\n`; } const next = render(); if (process.argv.includes("--check")) { const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, "utf8") : ""; if (current !== next) { console.error( "src/ui/settings/settings-search-index.ts is out of date. Run: node scripts/generate-settings-search-index.cjs", ); process.exit(1); } } else { fs.writeFileSync(OUT, next); console.log(`Wrote ${path.relative(ROOT, OUT)}`); }