mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add code health lint guards, consolidate relay constants, document agent prompts
Three CI-enforced lint scripts to prevent common codebase growth problems: - lint:file-size — enforces 1000-line max for new JS files, grandfathers existing oversized files and prevents them from growing further - lint:innerhtml — tracks innerHTML usage per-file with a baseline count, blocks new innerHTML additions (guides agents toward safe DOM APIs) - Relay constants consolidated from duplicate definitions in client.js and ConnectionManager.js into shared js/nostr/relayConstants.js - AGENTS.md Section 13: documents all three rules with examples - docs/scheduled-agent-prompts.md: copy-paste prompts for recurring file decomposition, innerHTML migration, and constant dedup tasks https://claude.ai/code/session_01LzErpH9FNw4hAPsjoYb1v9
This commit is contained in:
8 files changed
+534
-24
No files matched your search
@@ -249,6 +249,58 @@ _No reservations currently active._
|
||||
|
||||
---
|
||||
|
||||
## 13. Code Health Rules (CI-Enforced)
|
||||
|
||||
Three automated lint checks protect the codebase from common growth problems. These run in CI via `npm run lint` and will block your PR if violated.
|
||||
|
||||
### File Size Limits
|
||||
|
||||
**Script:** `npm run lint:file-size` (`scripts/check-file-size.mjs`)
|
||||
|
||||
- New `.js` files under `js/` must stay **under 1,000 lines**.
|
||||
- Existing oversized files are grandfathered with a recorded line count. They must **not grow** beyond their recorded count + 50 lines.
|
||||
- If your change would push a grandfathered file over its limit, **extract logic into a new module first**, then make your change.
|
||||
|
||||
**Decomposition strategy for agents:**
|
||||
1. Identify a cohesive block of logic (e.g., a group of related methods, a subsection of a controller).
|
||||
2. Extract it into a new file in the same directory.
|
||||
3. Export the extracted functions/class.
|
||||
4. Import and re-wire from the original file.
|
||||
5. Run `npm run lint:file-size` to confirm the original file shrank.
|
||||
6. Update the grandfathered count if needed (the maintainer will handle this when merging).
|
||||
|
||||
### innerHTML Baseline
|
||||
|
||||
**Script:** `npm run lint:innerhtml` (`scripts/check-innerhtml.mjs`)
|
||||
|
||||
- Every file has a **baseline** innerHTML assignment count. Adding new `innerHTML` usage beyond the baseline fails the lint.
|
||||
- For new UI code, use **safe DOM APIs** instead:
|
||||
```javascript
|
||||
// PREFERRED: auto-escaped, no XSS risk
|
||||
element.textContent = userProvidedString;
|
||||
const el = document.createElement("div");
|
||||
el.className = "card";
|
||||
parent.appendChild(el);
|
||||
|
||||
// ACCEPTABLE: innerHTML with escapeHtml for all user data
|
||||
import { escapeHtml } from "../utils/domUtils.js";
|
||||
container.innerHTML = `<span>${escapeHtml(title)}</span>`;
|
||||
|
||||
// BLOCKED: innerHTML with unescaped user data
|
||||
container.innerHTML = `<span>${title}</span>`; // XSS vulnerability
|
||||
```
|
||||
- If you must add innerHTML (e.g., loading a view template), update the baseline: `node scripts/check-innerhtml.mjs --update` and include the updated BASELINE in your PR.
|
||||
|
||||
### Constants — No Duplication
|
||||
|
||||
- Relay timing constants live in **`js/nostr/relayConstants.js`** — always import from there.
|
||||
- Before defining a new numeric constant, search for existing ones with the same value. If a match exists, import it.
|
||||
- Feature flags live in **`js/constants.js`**.
|
||||
- Cache policies live in **`js/nostr/cachePolicies.js`**.
|
||||
- Do not define the same constant in two files. If two modules need the same value, create a shared module and import from it.
|
||||
|
||||
---
|
||||
|
||||
## Next
|
||||
|
||||
Please read these documents next.
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Scheduled Agent Prompts
|
||||
|
||||
Copy-paste these prompts to schedule recurring maintenance tasks for AI agents.
|
||||
Each prompt is self-contained and targets one specific problem. Run them against
|
||||
the `unstable` branch.
|
||||
|
||||
---
|
||||
|
||||
## 1. File Decomposition (run weekly or biweekly)
|
||||
|
||||
**Best agent:** Claude Code (needs full codebase context for safe refactoring)
|
||||
|
||||
```
|
||||
Target the `unstable` branch. Run `node scripts/check-file-size.mjs --report`
|
||||
to see all grandfathered oversized files and their line counts.
|
||||
|
||||
Pick the SINGLE largest grandfathered file that has not been decomposed recently.
|
||||
Do NOT try to decompose multiple files in one PR.
|
||||
|
||||
For the chosen file:
|
||||
1. Read the entire file and identify 2-3 cohesive blocks of logic that can be
|
||||
extracted into separate modules (look for: groups of related helper functions,
|
||||
self-contained class methods that could become a separate service, rendering
|
||||
logic that could become its own controller).
|
||||
2. Extract each block into a new file in the same directory.
|
||||
3. Export the extracted code and import it from the original file.
|
||||
4. Run `npm run lint` and `npm run test:unit` to verify nothing broke.
|
||||
5. Update the grandfathered line count in `scripts/check-file-size.mjs` to
|
||||
reflect the new (smaller) size of the original file.
|
||||
6. Commit with message prefix `[decompose]` and create a PR targeting `unstable`.
|
||||
|
||||
Goal: reduce the file by at least 200 lines per session. Do not change any
|
||||
behavior — this is a pure extraction refactor.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. innerHTML Migration (run weekly or biweekly)
|
||||
|
||||
**Best agent:** Claude Code or Codex (scoped to individual files)
|
||||
|
||||
```
|
||||
Target the `unstable` branch. Run `node scripts/check-innerhtml.mjs --report`
|
||||
to see all files with innerHTML usage and their counts.
|
||||
|
||||
Pick ONE file from the list (prefer files with the highest count or files
|
||||
tagged RISKY in the audit). Do NOT try to migrate multiple files at once.
|
||||
|
||||
For the chosen file:
|
||||
1. Read the file and find all innerHTML assignments.
|
||||
2. For each assignment, determine if it can be replaced with safe DOM APIs:
|
||||
- Static text: use `element.textContent = "..."`.
|
||||
- Creating elements: use `document.createElement()` + `appendChild()`.
|
||||
- Template HTML with user data: ensure ALL interpolated values pass through
|
||||
`escapeHtml()` from `js/utils/domUtils.js`.
|
||||
3. Replace as many innerHTML assignments as possible with safe alternatives.
|
||||
4. Run `npm run lint` and `npm run test:unit` to verify nothing broke.
|
||||
5. Update the baseline: run `node scripts/check-innerhtml.mjs --update` and
|
||||
copy the new BASELINE object into `scripts/check-innerhtml.mjs`.
|
||||
6. Commit with message prefix `[security]` and create a PR targeting `unstable`.
|
||||
|
||||
Priority order for migration:
|
||||
- RISKY files first (user data without escaping)
|
||||
- High-count files next (profileModalController, searchView, channelProfile)
|
||||
- Low-count static files last (these are low risk)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Constant Deduplication Audit (run monthly)
|
||||
|
||||
**Best agent:** Any agent (simple search task)
|
||||
|
||||
```
|
||||
Target the `unstable` branch.
|
||||
|
||||
Search for numeric constants in `js/` that appear in multiple files with
|
||||
the same value and similar purpose. Focus on:
|
||||
- Timeout values (5000, 10000, 30000, 60000, etc.)
|
||||
- Retry counts
|
||||
- Cache TTLs
|
||||
- Threshold values
|
||||
|
||||
For each duplicate found:
|
||||
1. Determine the canonical location (check `js/constants.js`,
|
||||
`js/nostr/relayConstants.js`, `js/nostr/cachePolicies.js`).
|
||||
2. If a canonical location exists, replace the duplicate with an import.
|
||||
3. If no canonical location exists, create one in the most appropriate
|
||||
shared module.
|
||||
4. Run `npm run lint` and `npm run test:unit`.
|
||||
5. Commit with message prefix `[constants]` and create a PR targeting `unstable`.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Code Health Dashboard (run weekly)
|
||||
|
||||
**Best agent:** Any agent (read-only audit)
|
||||
|
||||
```
|
||||
Target the `unstable` branch. Run all three audit scripts in report mode:
|
||||
|
||||
1. `node scripts/check-file-size.mjs --report`
|
||||
2. `node scripts/check-innerhtml.mjs --report`
|
||||
3. `npm run lint`
|
||||
|
||||
Report the following metrics:
|
||||
- Total grandfathered oversized files and sum of excess lines
|
||||
- Total innerHTML assignments across all files
|
||||
- Any lint failures
|
||||
|
||||
Compare against the previous week's report (if available in issues/PRs).
|
||||
Post the results as a GitHub issue comment or a summary in the PR description
|
||||
so the maintainer can track progress.
|
||||
```
|
||||
+13
-11
@@ -274,17 +274,19 @@ function shouldRequestExtensionPermissions(signer) {
|
||||
return signer.type === "extension";
|
||||
}
|
||||
|
||||
const RELAY_CONNECT_TIMEOUT_MS = 5000;
|
||||
const RELAY_RECONNECT_BASE_DELAY_MS = 2000;
|
||||
const RELAY_RECONNECT_MAX_DELAY_MS = 60000;
|
||||
const RELAY_RECONNECT_MAX_ATTEMPTS = 5;
|
||||
const RELAY_BACKOFF_BASE_DELAY_MS = 1000;
|
||||
const RELAY_BACKOFF_MAX_DELAY_MS = 8000;
|
||||
const RELAY_CIRCUIT_BREAKER_THRESHOLD = 3;
|
||||
const RELAY_CIRCUIT_BREAKER_COOLDOWN_MS = 10 * 60 * 1000;
|
||||
const RELAY_FAILURE_WINDOW_MS = 5 * 60 * 1000;
|
||||
const RELAY_FAILURE_WINDOW_THRESHOLD = 3;
|
||||
const RELAY_SUMMARY_LOG_INTERVAL_MS = 30000;
|
||||
import {
|
||||
RELAY_CONNECT_TIMEOUT_MS,
|
||||
RELAY_RECONNECT_BASE_DELAY_MS,
|
||||
RELAY_RECONNECT_MAX_DELAY_MS,
|
||||
RELAY_RECONNECT_MAX_ATTEMPTS,
|
||||
RELAY_BACKOFF_BASE_DELAY_MS,
|
||||
RELAY_BACKOFF_MAX_DELAY_MS,
|
||||
RELAY_CIRCUIT_BREAKER_THRESHOLD,
|
||||
RELAY_CIRCUIT_BREAKER_COOLDOWN_MS,
|
||||
RELAY_FAILURE_WINDOW_MS,
|
||||
RELAY_FAILURE_WINDOW_THRESHOLD,
|
||||
RELAY_SUMMARY_LOG_INTERVAL_MS,
|
||||
} from "./relayConstants.js";
|
||||
const EVENTS_CACHE_STORAGE_KEY = "bitvid:eventsCache:v1";
|
||||
// We use the policy TTL, but currently the storage backend is hardcoded to IDB (with localStorage fallback).
|
||||
// Future refactors should make EventsCacheStore dynamic based on CACHE_POLICIES[NOTE_TYPES.VIDEO_POST].storage.
|
||||
|
||||
@@ -13,18 +13,19 @@ import {
|
||||
logCountTimeoutCleanupFailure,
|
||||
logRelayCountFailure,
|
||||
} from "../countDiagnostics.js";
|
||||
|
||||
const RELAY_CONNECT_TIMEOUT_MS = 5000;
|
||||
const RELAY_RECONNECT_BASE_DELAY_MS = 2000;
|
||||
const RELAY_RECONNECT_MAX_DELAY_MS = 60000;
|
||||
const RELAY_RECONNECT_MAX_ATTEMPTS = 5;
|
||||
const RELAY_BACKOFF_BASE_DELAY_MS = 1000;
|
||||
const RELAY_BACKOFF_MAX_DELAY_MS = 8000;
|
||||
const RELAY_CIRCUIT_BREAKER_THRESHOLD = 3;
|
||||
const RELAY_CIRCUIT_BREAKER_COOLDOWN_MS = 10 * 60 * 1000;
|
||||
const RELAY_FAILURE_WINDOW_MS = 5 * 60 * 1000;
|
||||
const RELAY_FAILURE_WINDOW_THRESHOLD = 3;
|
||||
const RELAY_SUMMARY_LOG_INTERVAL_MS = 30000;
|
||||
import {
|
||||
RELAY_CONNECT_TIMEOUT_MS,
|
||||
RELAY_RECONNECT_BASE_DELAY_MS,
|
||||
RELAY_RECONNECT_MAX_DELAY_MS,
|
||||
RELAY_RECONNECT_MAX_ATTEMPTS,
|
||||
RELAY_BACKOFF_BASE_DELAY_MS,
|
||||
RELAY_BACKOFF_MAX_DELAY_MS,
|
||||
RELAY_CIRCUIT_BREAKER_THRESHOLD,
|
||||
RELAY_CIRCUIT_BREAKER_COOLDOWN_MS,
|
||||
RELAY_FAILURE_WINDOW_MS,
|
||||
RELAY_FAILURE_WINDOW_THRESHOLD,
|
||||
RELAY_SUMMARY_LOG_INTERVAL_MS,
|
||||
} from "../relayConstants.js";
|
||||
|
||||
function withRequestTimeout(promise, timeoutMs, onTimeout, message = "Request timed out") {
|
||||
const resolvedTimeout = Number(timeoutMs);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// js/nostr/relayConstants.js
|
||||
// Canonical relay timing and circuit-breaker constants.
|
||||
// Import from here — do NOT duplicate these values in other modules.
|
||||
|
||||
export const RELAY_CONNECT_TIMEOUT_MS = 5000;
|
||||
export const RELAY_RECONNECT_BASE_DELAY_MS = 2000;
|
||||
export const RELAY_RECONNECT_MAX_DELAY_MS = 60000;
|
||||
export const RELAY_RECONNECT_MAX_ATTEMPTS = 5;
|
||||
export const RELAY_BACKOFF_BASE_DELAY_MS = 1000;
|
||||
export const RELAY_BACKOFF_MAX_DELAY_MS = 8000;
|
||||
export const RELAY_CIRCUIT_BREAKER_THRESHOLD = 3;
|
||||
export const RELAY_CIRCUIT_BREAKER_COOLDOWN_MS = 10 * 60 * 1000;
|
||||
export const RELAY_FAILURE_WINDOW_MS = 5 * 60 * 1000;
|
||||
export const RELAY_FAILURE_WINDOW_THRESHOLD = 3;
|
||||
export const RELAY_SUMMARY_LOG_INTERVAL_MS = 30000;
|
||||
+3
-1
@@ -16,7 +16,9 @@
|
||||
"ci:beacon": "npm run build:beacon",
|
||||
"format": "prettier --cache --write css/tailwind.source.css css/tokens.css css/docs.css \"docs/**/*.html\" tailwind.config.cjs README.md CONTRIBUTING.md",
|
||||
"audit": "node scripts/daily-design-system-audit.mjs",
|
||||
"lint": "npm run lint:css && npm run lint:hex && npm run lint:inline-styles && npm run lint:tokens && npm run lint:tailwind-brackets && npm run lint:tailwind-colors",
|
||||
"lint": "npm run lint:css && npm run lint:hex && npm run lint:inline-styles && npm run lint:tokens && npm run lint:tailwind-brackets && npm run lint:tailwind-colors && npm run lint:file-size && npm run lint:innerhtml",
|
||||
"lint:file-size": "node scripts/check-file-size.mjs",
|
||||
"lint:innerhtml": "node scripts/check-innerhtml.mjs",
|
||||
"lint:css": "stylelint css/tailwind.source.css css/tokens.css css/docs.css",
|
||||
"lint:hex": "node scripts/check-hex.js",
|
||||
"lint:inline-styles": "node scripts/check-inline-styles.mjs",
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Lint script: enforce maximum file size for JavaScript source files.
|
||||
*
|
||||
* Files that already exceed the threshold are grandfathered in an allowlist.
|
||||
* The allowlist records each file's line count at the time it was added.
|
||||
* A grandfathered file may NOT grow — if its current line count exceeds the
|
||||
* recorded count, the check fails. New files that exceed the threshold
|
||||
* without an allowlist entry also fail.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-file-size.mjs # enforce (exit 1 on failure)
|
||||
* node scripts/check-file-size.mjs --report # report only (exit 0)
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const ROOT = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
||||
const SOURCE_DIR = join(ROOT, "js");
|
||||
const THRESHOLD = 1000; // lines — new files must stay under this
|
||||
const GROWTH_MARGIN = 50; // lines — allowlisted files may grow by this much for minor fixes
|
||||
|
||||
// Grandfathered files: path relative to repo root → line count when recorded.
|
||||
// These files are known to be oversized and are decomposition targets.
|
||||
// They must NOT grow beyond (recorded + GROWTH_MARGIN).
|
||||
const GRANDFATHERED = {
|
||||
"js/ui/profileModalController.js": 14159,
|
||||
"js/ui/components/VideoModal.js": 6284,
|
||||
"js/app.js": 5602,
|
||||
"js/channelProfile.js": 5529,
|
||||
"js/nostr/client.js": 4356,
|
||||
"js/ui/components/VideoCard.js": 3129,
|
||||
"js/nostrEventSchemas.js": 2906,
|
||||
"js/ui/loginModalController.js": 2552,
|
||||
"js/historyView.js": 2545,
|
||||
"js/services/moderationService.js": 2512,
|
||||
"js/subscriptions.js": 2374,
|
||||
"js/userBlocks.js": 2297,
|
||||
"js/services/nostrService.js": 2227,
|
||||
"js/nostr/watchHistory.js": 2211,
|
||||
"js/nostr/nip46Client.js": 2102,
|
||||
"js/state/cache.js": 1827,
|
||||
"js/app/feedCoordinator.js": 1767,
|
||||
"js/app/authSessionCoordinator.js": 1743,
|
||||
"js/ui/components/RevertModal.js": 1663,
|
||||
"js/payments/nwcClient.js": 1642,
|
||||
"js/ui/moreMenuController.js": 1601,
|
||||
"js/nostr/commentEvents.js": 1571,
|
||||
"js/nostr/nip71.js": 1531,
|
||||
"js/services/hashtagPreferencesService.js": 1434,
|
||||
"js/nostr/publishHelpers.js": 1415,
|
||||
"js/index.js": 1412,
|
||||
"js/watchHistoryService.js": 1395,
|
||||
"js/app/playbackCoordinator.js": 1376,
|
||||
"js/feedEngine/stages.js": 1370,
|
||||
"js/ui/views/VideoListView.js": 1359,
|
||||
"js/ui/components/UploadModal.js": 1319,
|
||||
"js/ui/components/SimilarContentCard.js": 1303,
|
||||
"js/services/playbackService.js": 1289,
|
||||
"js/services/authService.js": 1276,
|
||||
"js/ui/zapController.js": 1206,
|
||||
"js/services/r2Service.js": 1194,
|
||||
"js/ui/applicationBootstrap.js": 1163,
|
||||
"js/ui/components/EditModal.js": 1130,
|
||||
"js/services/commentThreadService.js": 1122,
|
||||
"js/nostr/managers/SignerManager.js": 1106,
|
||||
"js/nostr/nip46Connector.js": 1094,
|
||||
"js/ui/overlay/popoverEngine.js": 1070,
|
||||
"js/adminListStore.js": 1033,
|
||||
};
|
||||
|
||||
function collectJsFiles(dir) {
|
||||
const results = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...collectJsFiles(full));
|
||||
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function countLines(filePath) {
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
return content.split("\n").length;
|
||||
}
|
||||
|
||||
const reportOnly = process.argv.includes("--report");
|
||||
const files = collectJsFiles(SOURCE_DIR);
|
||||
const violations = [];
|
||||
const warnings = [];
|
||||
|
||||
for (const filePath of files) {
|
||||
const relPath = relative(ROOT, filePath);
|
||||
const lines = countLines(filePath);
|
||||
|
||||
if (relPath in GRANDFATHERED) {
|
||||
const recorded = GRANDFATHERED[relPath];
|
||||
const limit = recorded + GROWTH_MARGIN;
|
||||
if (lines > limit) {
|
||||
violations.push(
|
||||
`GREW: ${relPath} (${lines} lines, was ${recorded}, limit ${limit})`,
|
||||
);
|
||||
} else if (lines > THRESHOLD) {
|
||||
warnings.push(`grandfathered: ${relPath} (${lines} lines)`);
|
||||
}
|
||||
} else if (lines > THRESHOLD) {
|
||||
violations.push(`NEW: ${relPath} (${lines} lines, threshold ${THRESHOLD})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length > 0 && (reportOnly || violations.length > 0)) {
|
||||
console.log(`\n${warnings.length} grandfathered oversized file(s):`);
|
||||
for (const w of warnings) {
|
||||
console.log(` ⚠ ${w}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(`\n${violations.length} file size violation(s):`);
|
||||
for (const v of violations) {
|
||||
console.error(` ✗ ${v}`);
|
||||
}
|
||||
console.error(
|
||||
`\nNew files must stay under ${THRESHOLD} lines. Grandfathered files must not grow.`,
|
||||
);
|
||||
console.error(
|
||||
"To decompose a large file, extract logic into smaller modules and re-export.",
|
||||
);
|
||||
if (!reportOnly) {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
if (reportOnly && warnings.length > 0) {
|
||||
console.log(
|
||||
`\nNo violations. ${warnings.length} grandfathered file(s) remain as decomposition targets.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Lint script: audit innerHTML usage in JavaScript source files.
|
||||
*
|
||||
* Maintains a baseline count of innerHTML assignments per file. If a file
|
||||
* introduces NEW innerHTML usage beyond its baseline, the check fails.
|
||||
* This enforces a "no new innerHTML" policy while grandfathering existing usage.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-innerhtml.mjs # enforce (exit 1 on new usage)
|
||||
* node scripts/check-innerhtml.mjs --report # report all usage (exit 0)
|
||||
* node scripts/check-innerhtml.mjs --update # update baseline to current counts
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, readdirSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const ROOT = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
||||
const SOURCE_DIR = join(ROOT, "js");
|
||||
|
||||
// Baseline: file path (relative to repo root) → known innerHTML assignment count.
|
||||
// Generated with --update flag. Files not listed have a baseline of 0.
|
||||
const BASELINE = {
|
||||
"js/app.js": 1,
|
||||
"js/app/feedCoordinator.js": 1,
|
||||
"js/channelProfile.js": 10,
|
||||
"js/docsView.js": 8,
|
||||
"js/exploreView.js": 1,
|
||||
"js/forYouView.js": 1,
|
||||
"js/historyView.js": 4,
|
||||
"js/index.js": 1,
|
||||
"js/kidsView.js": 1,
|
||||
"js/searchView.js": 12,
|
||||
"js/subscriptions.js": 7,
|
||||
"js/ui/components/DeleteModal.js": 3,
|
||||
"js/ui/components/EditModal.js": 1,
|
||||
"js/ui/components/EmbedVideoModal.js": 1,
|
||||
"js/ui/components/EventDetailsModal.js": 1,
|
||||
"js/ui/components/nip71FormManager.js": 1,
|
||||
"js/ui/components/RevertModal.js": 10,
|
||||
"js/ui/components/ShareNostrModal.js": 3,
|
||||
"js/ui/components/UploadModal.js": 1,
|
||||
"js/ui/components/VideoModal.js": 1,
|
||||
"js/ui/dm/AppShell.js": 1,
|
||||
"js/ui/dm/Composer.js": 1,
|
||||
"js/ui/dm/ConversationList.js": 1,
|
||||
"js/ui/dm/DMRelaySettings.js": 1,
|
||||
"js/ui/dm/DMSettingsModalController.js": 3,
|
||||
"js/ui/dm/MessageThread.js": 1,
|
||||
"js/ui/dm/NotificationCenter.js": 1,
|
||||
"js/ui/loginModalController.js": 3,
|
||||
"js/ui/moreMenuController.js": 1,
|
||||
"js/ui/profileModalController.js": 27,
|
||||
"js/ui/subscriptionHistoryController.js": 7,
|
||||
"js/ui/videoListViewController.js": 1,
|
||||
"js/ui/views/VideoListView.js": 3,
|
||||
"js/utils/qrcode.js": 2,
|
||||
"js/viewManager.js": 3,
|
||||
};
|
||||
|
||||
// Pattern to match innerHTML assignments: .innerHTML = or .innerHTML +=
|
||||
const PATTERN = /\.innerHTML\s*[+]?=/g;
|
||||
|
||||
function collectJsFiles(dir) {
|
||||
const results = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...collectJsFiles(full));
|
||||
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function countInnerHtmlUsage(filePath) {
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
const matches = content.match(PATTERN);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
function findInnerHtmlLines(filePath) {
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
const lines = content.split("\n");
|
||||
const results = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (PATTERN.test(lines[i])) {
|
||||
results.push(i + 1);
|
||||
}
|
||||
// Reset regex lastIndex since we use global flag
|
||||
PATTERN.lastIndex = 0;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const mode = process.argv.includes("--update")
|
||||
? "update"
|
||||
: process.argv.includes("--report")
|
||||
? "report"
|
||||
: "enforce";
|
||||
|
||||
const files = collectJsFiles(SOURCE_DIR);
|
||||
const violations = [];
|
||||
const currentCounts = {};
|
||||
|
||||
for (const filePath of files) {
|
||||
const relPath = relative(ROOT, filePath);
|
||||
const count = countInnerHtmlUsage(filePath);
|
||||
|
||||
if (count > 0) {
|
||||
currentCounts[relPath] = count;
|
||||
}
|
||||
|
||||
const baseline = BASELINE[relPath] || 0;
|
||||
|
||||
if (count > baseline) {
|
||||
const lineNums = findInnerHtmlLines(filePath);
|
||||
violations.push({
|
||||
file: relPath,
|
||||
count,
|
||||
baseline,
|
||||
added: count - baseline,
|
||||
lines: lineNums,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "update") {
|
||||
// Print updated baseline for copy-pasting into this script
|
||||
console.log("// Updated BASELINE — copy this into check-innerhtml.mjs:");
|
||||
console.log("const BASELINE = {");
|
||||
const sorted = Object.entries(currentCounts).sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
for (const [file, count] of sorted) {
|
||||
console.log(` "${file}": ${count},`);
|
||||
}
|
||||
console.log("};");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (mode === "report") {
|
||||
const total = Object.values(currentCounts).reduce((a, b) => a + b, 0);
|
||||
console.log(`innerHTML usage: ${total} assignments across ${Object.keys(currentCounts).length} files\n`);
|
||||
const sorted = Object.entries(currentCounts).sort(([, a], [, b]) => b - a);
|
||||
for (const [file, count] of sorted) {
|
||||
const base = BASELINE[file] || 0;
|
||||
const status = count > base ? " ← NEW" : "";
|
||||
console.log(` ${file}: ${count}${status}`);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Enforce mode
|
||||
if (violations.length > 0) {
|
||||
console.error(`${violations.length} file(s) introduced new innerHTML usage:\n`);
|
||||
for (const v of violations) {
|
||||
console.error(
|
||||
` ✗ ${v.file}: ${v.count} total (baseline ${v.baseline}, +${v.added} new) at line(s) ${v.lines.join(", ")}`,
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
"\nUse safe DOM APIs instead of innerHTML for new code:",
|
||||
);
|
||||
console.error(
|
||||
" element.textContent = text; // plain text (auto-escaped) ",
|
||||
);
|
||||
console.error(
|
||||
" element.appendChild(doc.createElement()); // structured DOM building",
|
||||
);
|
||||
console.error(
|
||||
"\nIf innerHTML is truly needed, use escapeHtml() for all interpolated values.",
|
||||
);
|
||||
console.error(
|
||||
"To update the baseline after intentional changes: node scripts/check-innerhtml.mjs --update",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in new issue
Block a user