From ceccd3468119b588ba21e4865800ae79718776ac Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:51:01 -0500 Subject: [PATCH] Revert "Dead Code Cleanup" --- artifacts/depcheck.json | 2 +- js/adminListStore.js | 2 +- js/config.js | 3 + js/magnets.js | 89 ++++++++++++++++++++++++++- js/nostr/nip71.js | 2 +- js/nostr/toolkit.js | 2 + js/themeController.js | 14 ++++- js/utils/qrcode.js | 46 +++++++++++++- scripts/agent/find-unused-exports.mjs | 73 ---------------------- tests/admin-list-store.test.mjs | 2 - 10 files changed, 154 insertions(+), 81 deletions(-) delete mode 100644 scripts/agent/find-unused-exports.mjs diff --git a/artifacts/depcheck.json b/artifacts/depcheck.json index eb45ee0c..4990648a 100644 --- a/artifacts/depcheck.json +++ b/artifacts/depcheck.json @@ -1 +1 @@ -{"dependencies":["autoprefixer","postcss","tailwindcss"],"devDependencies":["autoprefixer","cross-env","prettier","serve","stylelint"],"missing":{"@scure/base":["/app/vendor/crypto-helpers.entry.js","/app/tests/zap-split.test.mjs"],"https:":["/app/js/nostrToolsBootstrap.js"]},"using":{"playwright":["/app/debug_sidebar.mjs","/app/tests/embed_playback_check.spec.mjs","/app/scripts/agent/smoke-test.mjs"],"@playwright/test":["/app/playwright.config.ts","/app/tests/visual/embed-layout.spec.ts","/app/tests/visual/kitchen-sink.spec.ts","/app/tests/visual/moderation.spec.ts","/app/tests/visual/overlay-layers.spec.ts","/app/tests/visual/reduced-motion.spec.ts","/app/tests/e2e/popover.spec.ts","/app/tests/e2e/repro_issue.spec.ts","/app/tests/e2e/toast-service.spec.ts","/app/tests/e2e/ui-modals.spec.ts","/app/tests/e2e/video-modal-share-bug.spec.ts","/app/tests/e2e/webseed-playback.spec.ts","/app/tests/e2e/helpers/uiTestUtils.ts","/app/examples/reproducers/playwright.config.ts","/app/examples/reproducers/popover-flip-bug/repro.spec.ts","/app/examples/reproducers/issue-sidebar-layout/repro.spec.ts","/app/examples/reproducers/issue-popover-viewport/repro.spec.ts","/app/examples/reproducers/issue-moderation-visibility/repro.spec.ts","/app/examples/reproducers/issue-moderation-ui/playwright.config.ts","/app/examples/reproducers/issue-moderation-ui/repro.spec.ts","/app/examples/reproducers/issue-1-moderation-ui/playwright.config.ts","/app/examples/reproducers/issue-1-moderation-ui/repro.spec.ts","/app/examples/reproducers/issue-003-popover-ref-error/repro.spec.ts","/app/examples/reproducers/issue-002-login-modal-nsec/repro.spec.ts","/app/examples/reproducers/003-video-modal-reg/repro.spec.ts","/app/examples/reproducers/002-sidebar-layout/repro.spec.ts","/app/examples/reproducers/001-popover-404/repro.spec.ts"],"prettier-plugin-tailwindcss":["/app/prettier.config.cjs"],"@tailwindcss/typography":["/app/tailwind.config.cjs"],"@scure/base":["/app/vendor/crypto-helpers.entry.js","/app/tests/zap-split.test.mjs"],"@noble/hashes":["/app/vendor/crypto-helpers.entry.js"],"jsdom":["/app/tests/design-system-metrics.test.mjs","/app/tests/discussion-count-service.test.mjs","/app/tests/edit-modal-submit-state.test.mjs","/app/tests/login-modal-controller.test.mjs","/app/tests/media-loader.test.mjs","/app/tests/modal-accessibility.test.mjs","/app/tests/nip71-form-manager.test.mjs","/app/tests/position-floating-panel.test.mjs","/app/tests/profile-modal-controller.test.mjs","/app/tests/subscriptions-manager.test.mjs","/app/tests/test-setup.mjs","/app/tests/video-card-source-visibility.test.mjs","/app/tests/video-modal-accessibility.test.mjs","/app/tests/ui/app-chrome-controller.test.mjs","/app/tests/ui/hashtag-strip-helper-bounce.test.mjs","/app/tests/ui/popoverEngine.test.mjs","/app/tests/ui/profile-modal-moderation-settings.test.mjs","/app/tests/ui/tag-pill-list.test.mjs","/app/tests/ui/tag-preference-menu.test.mjs","/app/tests/ui/url-health-controller.test.mjs","/app/tests/ui/video-list-view-popular-tags.test.mjs","/app/tests/ui/video-list-view-sorting.test.mjs","/app/tests/ui/helpers/jsdom-test-helpers.mjs","/app/tests/torrent/style-helpers.test.mjs","/app/tests/torrent/toast-service.test.mjs","/app/tests/services/link-preview-service.test.mjs","/app/tests/moderation/video-card.test.mjs","/app/tests/app/channel-profile-moderation.test.mjs","/app/tests/app/hash-change-handler.test.mjs","/app/tests/app/hydrate-sidebar-navigation.test.mjs","/app/examples/reproducers/video-card-bug/repro.mjs","/app/examples/reproducers/issue-channel-moderation-test-crash/repro.mjs","/app/examples/reproducers/issue-001-video-card-logic/repro.mjs"],"fake-indexeddb":["/app/tests/dm-db.test.mjs","/app/tests/dm-normalization.test.mjs","/app/tests/nostr-session-actor.test.mjs","/app/tests/storage-service.test.mjs","/app/tests/ui/storageService.test.mjs","/app/tests/services/link-preview-service.test.mjs","/app/tests/nostr/dm-direct-message-flow.test.mjs","/app/tests/nostr/session-actor.test.mjs"],"nostr-tools":["/app/tests/dm-decryptor.test.mjs","/app/tests/nostr-private-key-signer.test.mjs","/app/tests/nostr-publish-rejection.test.mjs","/app/tests/nostr-send-direct-message.test.mjs","/app/tests/nostr/adapters.test.mjs","/app/tests/nostr/dm-direct-message-flow.test.mjs","/app/tests/nostr/dmSignalEvents.test.mjs","/app/tests/nostr/integration-remote-flow.test.mjs","/app/tests/nostr/nip46Client.test.js","/app/tests/e2e/repro_issue.spec.ts","/app/scripts/agent/interop-test.mjs","/app/scripts/agent/load-test-relay.mjs","/app/scripts/agent/load-test.mjs","/app/scripts/agent/simple-relay.mjs","/app/scripts/agent/smoke-test.mjs","/app/js/nostrToolsBootstrap.js"],"pixelmatch":["/app/tests/visual/kitchen-sink.spec.ts"],"pngjs":["/app/tests/visual/kitchen-sink.spec.ts"],"ws":["/app/tests/e2e/repro_issue.spec.ts","/app/scripts/agent/load-test-relay.mjs","/app/scripts/agent/load-test.mjs","/app/scripts/agent/setup-test-env.js","/app/scripts/agent/simple-relay.mjs","/app/scripts/agent/validate-events.mjs"],"esbuild":["/app/scripts/build-beacon.mjs"],"@floating-ui/dom":["/app/scripts/build-beacon.mjs","/app/scripts/vendor/floating-ui-dom-entry.mjs"],"https:":["/app/js/nostrToolsBootstrap.js"]},"invalidFiles":{},"invalidDirs":{}} +{"dependencies":[],"devDependencies":["autoprefixer","cross-env","postcss","prettier","serve","stylelint","tailwindcss"],"missing":{"@scure/base":["/app/vendor/crypto-helpers.entry.js","/app/tests/zap-split.test.mjs"],"https:":["/app/js/nostrToolsBootstrap.js"]},"using":{"playwright":["/app/debug_sidebar.mjs","/app/tests/embed_playback_check.spec.mjs"],"@playwright/test":["/app/playwright.config.ts","/app/tests/visual/embed-layout.spec.ts","/app/tests/visual/kitchen-sink.spec.ts","/app/tests/visual/moderation.spec.ts","/app/tests/visual/overlay-layers.spec.ts","/app/tests/visual/reduced-motion.spec.ts","/app/tests/e2e/popover.spec.ts","/app/tests/e2e/repro_issue.spec.ts","/app/tests/e2e/toast-service.spec.ts","/app/tests/e2e/ui-modals.spec.ts","/app/tests/e2e/video-modal-share-bug.spec.ts","/app/tests/e2e/webseed-playback.spec.ts","/app/tests/e2e/helpers/uiTestUtils.ts","/app/examples/reproducers/playwright.config.ts","/app/examples/reproducers/issue-sidebar-layout/repro.spec.ts","/app/examples/reproducers/issue-popover-viewport/repro.spec.ts","/app/examples/reproducers/issue-moderation-visibility/repro.spec.ts","/app/examples/reproducers/issue-moderation-ui/playwright.config.ts","/app/examples/reproducers/issue-moderation-ui/repro.spec.ts","/app/examples/reproducers/issue-1-moderation-ui/playwright.config.ts","/app/examples/reproducers/issue-1-moderation-ui/repro.spec.ts","/app/examples/reproducers/issue-003-popover-ref-error/repro.spec.ts","/app/examples/reproducers/issue-002-login-modal-nsec/repro.spec.ts","/app/examples/reproducers/003-video-modal-reg/repro.spec.ts","/app/examples/reproducers/002-sidebar-layout/repro.spec.ts","/app/examples/reproducers/001-popover-404/repro.spec.ts"],"prettier-plugin-tailwindcss":["/app/prettier.config.cjs"],"@tailwindcss/typography":["/app/tailwind.config.cjs"],"@scure/base":["/app/vendor/crypto-helpers.entry.js","/app/tests/zap-split.test.mjs"],"@noble/hashes":["/app/vendor/crypto-helpers.entry.js"],"jsdom":["/app/tests/design-system-metrics.test.mjs","/app/tests/discussion-count-service.test.mjs","/app/tests/edit-modal-submit-state.test.mjs","/app/tests/login-modal-controller.test.mjs","/app/tests/media-loader.test.mjs","/app/tests/modal-accessibility.test.mjs","/app/tests/nip71-form-manager.test.mjs","/app/tests/position-floating-panel.test.mjs","/app/tests/profile-modal-controller.test.mjs","/app/tests/subscriptions-manager.test.mjs","/app/tests/test-setup.mjs","/app/tests/video-card-source-visibility.test.mjs","/app/tests/video-modal-accessibility.test.mjs","/app/tests/ui/app-chrome-controller.test.mjs","/app/tests/ui/hashtag-strip-helper-bounce.test.mjs","/app/tests/ui/popoverEngine.test.mjs","/app/tests/ui/profile-modal-moderation-settings.test.mjs","/app/tests/ui/tag-pill-list.test.mjs","/app/tests/ui/tag-preference-menu.test.mjs","/app/tests/ui/url-health-controller.test.mjs","/app/tests/ui/video-list-view-popular-tags.test.mjs","/app/tests/ui/video-list-view-sorting.test.mjs","/app/tests/ui/helpers/jsdom-test-helpers.mjs","/app/tests/torrent/style-helpers.test.mjs","/app/tests/torrent/toast-service.test.mjs","/app/tests/services/link-preview-service.test.mjs","/app/tests/moderation/video-card.test.mjs","/app/tests/app/channel-profile-moderation.test.mjs","/app/tests/app/hash-change-handler.test.mjs","/app/tests/app/hydrate-sidebar-navigation.test.mjs","/app/examples/reproducers/issue-channel-moderation-test-crash/repro.mjs","/app/examples/reproducers/issue-001-video-card-logic/repro.mjs"],"fake-indexeddb":["/app/tests/dm-db.test.mjs","/app/tests/dm-normalization.test.mjs","/app/tests/nostr-session-actor.test.mjs","/app/tests/storage-service.test.mjs","/app/tests/ui/storageService.test.mjs","/app/tests/services/link-preview-service.test.mjs","/app/tests/nostr/dm-direct-message-flow.test.mjs","/app/tests/nostr/session-actor.test.mjs","/app/scripts/agent/smoke-test.mjs"],"nostr-tools":["/app/tests/dm-decryptor.test.mjs","/app/tests/nostr-private-key-signer.test.mjs","/app/tests/nostr-publish-rejection.test.mjs","/app/tests/nostr-send-direct-message.test.mjs","/app/tests/nostr/adapters.test.mjs","/app/tests/nostr/dm-direct-message-flow.test.mjs","/app/tests/nostr/dmSignalEvents.test.mjs","/app/tests/nostr/integration-remote-flow.test.mjs","/app/tests/nostr/nip46Client.test.js","/app/tests/e2e/repro_issue.spec.ts","/app/scripts/agent/interop-test.mjs","/app/scripts/agent/load-test-relay.mjs","/app/scripts/agent/load-test.mjs","/app/scripts/agent/simple-relay.mjs","/app/scripts/agent/smoke-test.mjs","/app/js/nostrToolsBootstrap.js"],"pixelmatch":["/app/tests/visual/kitchen-sink.spec.ts"],"pngjs":["/app/tests/visual/kitchen-sink.spec.ts"],"ws":["/app/tests/e2e/repro_issue.spec.ts","/app/scripts/agent/interop-test.mjs","/app/scripts/agent/load-test-relay.mjs","/app/scripts/agent/load-test.mjs","/app/scripts/agent/setup-test-env.js","/app/scripts/agent/simple-relay.mjs","/app/scripts/agent/smoke-test.mjs"],"esbuild":["/app/scripts/build-beacon.mjs"],"@floating-ui/dom":["/app/scripts/build-beacon.mjs","/app/scripts/vendor/floating-ui-dom-entry.mjs"],"https:":["/app/js/nostrToolsBootstrap.js"]},"invalidFiles":{},"invalidDirs":{}} diff --git a/js/adminListStore.js b/js/adminListStore.js index c3ca3698..afed1d2f 100644 --- a/js/adminListStore.js +++ b/js/adminListStore.js @@ -270,7 +270,7 @@ export const readCachedAdminState = () => { } }; -const writeCachedAdminState = (state) => { +export const writeCachedAdminState = (state) => { const policy = CACHE_POLICIES[NOTE_TYPES.ADMIN_MODERATION_LIST]; if (policy?.storage !== STORAGE_TIERS.LOCAL_STORAGE) { return; diff --git a/js/config.js b/js/config.js index 6e32bee7..d8c31343 100644 --- a/js/config.js +++ b/js/config.js @@ -80,8 +80,10 @@ export const isVerboseDevMode = verboseDevModeOverride === null ? Boolean(IS_VERBOSE_DEV_MODE) : verboseDevModeOverride; +export { IS_VERBOSE_DEV_MODE }; export const isLockdownMode = Boolean(IS_LOCKDOWN_MODE); +export { IS_LOCKDOWN_MODE }; export { LOCKDOWN_TITLE, LOCKDOWN_MESSAGE }; if (typeof window !== "undefined") { @@ -108,6 +110,7 @@ export { TIP_JAR_URL }; export { MAX_WALLET_DEFAULT_ZAP }; export { PLATFORM_FEE_PERCENT }; export { PLATFORM_LUD16_OVERRIDE }; +export { DEFAULT_RELAY_URLS_OVERRIDE }; export const ADMIN_EDITORS_NPUBS = []; // Default moderators (optional) export const ADMIN_LIST_NAMESPACE = "bitvid:admin"; // Reserved for Nostr lists export const ADMIN_COMMUNITY_BLACKLIST_SOURCES = "community:blacklist:sources"; diff --git a/js/magnets.js b/js/magnets.js index 05a32885..c280014d 100644 --- a/js/magnets.js +++ b/js/magnets.js @@ -1,4 +1,23 @@ -import { extractBtihFromMagnet } from "./magnetShared.js"; +import { extractBtihFromMagnet, normalizeInfoHash } from "./magnetShared.js"; + +function extractInfoHashFromXt(value) { + if (typeof value !== "string") { + return ""; + } + + const trimmed = value.trim(); + if (!trimmed) { + return ""; + } + + const lower = trimmed.toLowerCase(); + if (!lower.startsWith("urn:btih:")) { + return ""; + } + + const payload = trimmed.slice(9); + return normalizeInfoHash(payload); +} export function infoHashFromMagnet(magnet) { if (typeof magnet !== "string") { @@ -7,3 +26,71 @@ export function infoHashFromMagnet(magnet) { const extracted = extractBtihFromMagnet(magnet); return extracted || null; } + +export function trackersFromMagnet(magnet) { + if (typeof magnet !== "string") { + return []; + } + const parsed = parseMagnetLite(magnet); + if (!parsed || !Array.isArray(parsed.announce)) { + return []; + } + const deduped = new Set(); + parsed.announce.forEach((url) => { + if (typeof url !== "string") { + return; + } + const trimmed = url.trim(); + if (!trimmed) { + return; + } + deduped.add(trimmed); + }); + return Array.from(deduped); +} + +function parseMagnetLite(magnet) { + if (typeof magnet !== "string") { + return { infoHash: "", announce: [] }; + } + + const trimmed = magnet.trim(); + if (!trimmed) { + return { infoHash: "", announce: [] }; + } + + const lower = trimmed.toLowerCase(); + const queryIndex = lower.indexOf("?"); + const paramsPart = queryIndex >= 0 ? trimmed.slice(queryIndex + 1) : ""; + const params = paramsPart.split("&"); + const announce = []; + let infoHash = ""; + + for (const param of params) { + if (!param) { + continue; + } + + const [rawKey, ...rawValueParts] = param.split("="); + const rawValue = rawValueParts.join("="); + const key = decodeURIComponent((rawKey || "").replace(/\+/g, " ")).toLowerCase(); + const value = decodeURIComponent((rawValue || "").replace(/\+/g, " ")); + + if (!key) { + continue; + } + + if (key === "xt") { + const derived = extractInfoHashFromXt(value); + if (derived && !infoHash) { + infoHash = derived; + } + } else if (key === "tr") { + if (value) { + announce.push(value); + } + } + } + + return { infoHash, announce }; +} diff --git a/js/nostr/nip71.js b/js/nostr/nip71.js index 1517cb60..25e5cf6a 100644 --- a/js/nostr/nip71.js +++ b/js/nostr/nip71.js @@ -843,7 +843,7 @@ export function buildVideoPointerValue(pubkey, videoRootId) { return `30078:${normalizedPubkey}:${normalizedRoot}`; } -function buildNip71PointerTags({ +export function buildNip71PointerTags({ pubkey = "", videoRootId = "", videoEventId = "", diff --git a/js/nostr/toolkit.js b/js/nostr/toolkit.js index d230c5dc..4ee1a134 100644 --- a/js/nostr/toolkit.js +++ b/js/nostr/toolkit.js @@ -335,6 +335,8 @@ export function resolveSimplePoolConstructor(tools, scope = globalScope) { return null; } +export { isSimplePoolConstructor, unwrapSimplePool }; + export function shimLegacySimplePoolMethods(pool) { if (!pool || typeof pool !== "object" || pool[SIMPLE_POOL_SHIM_KEY]) { return pool || null; diff --git a/js/themeController.js b/js/themeController.js index 2637ac17..c5858696 100644 --- a/js/themeController.js +++ b/js/themeController.js @@ -307,7 +307,19 @@ const applyTheme = (theme, { persist = true } = {}) => { return normalized; }; -const setTheme = (theme) => { +export const setThemeAccentOverrides = (overrides) => { + setAccentOverrides(overrides); + + const root = getRootElement(); + if (!root) { + return; + } + + const theme = currentTheme || FALLBACK_THEME; + applyAccentOverrides(root, theme); +}; + +export const setTheme = (theme) => { if (!isBrowser()) { currentTheme = normalizeTheme(theme) || FALLBACK_THEME; return currentTheme; diff --git a/js/utils/qrcode.js b/js/utils/qrcode.js index a86e4239..20bb2932 100644 --- a/js/utils/qrcode.js +++ b/js/utils/qrcode.js @@ -3,7 +3,10 @@ const hasDocument = typeof document.createElement === "function" && typeof document.createElementNS === "function"; +let QRCodeExport = null; +let createQrCodeExport = null; let renderQrCodeExport = null; +let updateQrCodeExport = null; if (hasDocument) { /** @@ -747,15 +750,56 @@ if (hasDocument) { QRCode.CorrectLevel = QRErrorCorrectLevel; })(); + QRCodeExport = QRCode; + + createQrCodeExport = function createQrCode(element, options = {}) { + return new QRCode(element, options); + }; + renderQrCodeExport = function renderQrCode(element, text, options = {}) { const normalizedOptions = Object.assign({}, options, { text }); const instance = new QRCode(element, normalizedOptions); return instance; }; + + updateQrCodeExport = function updateQrCode(instance, text) { + if (!instance || typeof instance.makeCode !== "function") { + throw new Error("Invalid QRCode instance"); + } + instance.makeCode(text); + return instance; + }; } else { + class QRCodeStub { + constructor() { + throw new Error("QR code rendering requires a DOM environment."); + } + + makeCode() { + throw new Error("QR code rendering requires a DOM environment."); + } + + clear() {} + } + + QRCodeExport = QRCodeStub; + + createQrCodeExport = function createQrCodeStub() { + throw new Error("QR code rendering requires a DOM environment."); + }; + renderQrCodeExport = function renderQrCodeStub() { throw new Error("QR code rendering requires a DOM environment."); }; + + updateQrCodeExport = function updateQrCodeStub() { + throw new Error("QR code rendering requires a DOM environment."); + }; } -export { renderQrCodeExport as renderQrCode }; +export { QRCodeExport as QRCode }; +export { + createQrCodeExport as createQrCode, + renderQrCodeExport as renderQrCode, + updateQrCodeExport as updateQrCode, +}; diff --git a/scripts/agent/find-unused-exports.mjs b/scripts/agent/find-unused-exports.mjs deleted file mode 100644 index 751385a7..00000000 --- a/scripts/agent/find-unused-exports.mjs +++ /dev/null @@ -1,73 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { execSync } from 'child_process'; - -function getExportedSymbols(filePath) { - const content = fs.readFileSync(filePath, 'utf-8'); - const symbols = []; - - // Regex to capture exported names - const exportRegex = /export\s+(?:const|let|var|function|class)\s+([a-zA-Z0-9_$]+)/g; - let match; - while ((match = exportRegex.exec(content)) !== null) { - symbols.push(match[1]); - } - - // Handle export default - if (/export\s+default/.test(content)) { - // For default export, we can't easily get a name to grep for usage unless we look at imports. - // We'll skip default exports for this simple heuristic script. - } - - // Handle export { name } - const exportListRegex = /export\s+\{([^}]+)\}/g; - while ((match = exportListRegex.exec(content)) !== null) { - const names = match[1].split(',').map(s => s.trim().split(' as ')[0].trim()); - symbols.push(...names); - } - - return symbols; -} - -function findUsages(symbol, filePath) { - // Grep for the symbol in the whole js directory, excluding the defining file - // We use word boundary \b to avoid partial matches - try { - const cmd = `grep -r "\\b${symbol}\\b" js --exclude="${path.basename(filePath)}" | wc -l`; - const count = parseInt(execSync(cmd, { encoding: 'utf-8' }).trim()); - return count; - } catch (e) { - return 0; - } -} - -function scanDir(dir) { - const files = fs.readdirSync(dir); - for (const file of files) { - const fullPath = path.join(dir, file); - const stat = fs.statSync(fullPath); - if (stat.isDirectory()) { - scanDir(fullPath); - } else if (file.endsWith('.js') || file.endsWith('.mjs')) { - const symbols = getExportedSymbols(fullPath); - for (const symbol of symbols) { - const usages = findUsages(symbol, fullPath); - if (usages === 0) { - // Check if it is used in tests - try { - const testCmd = `grep -r "\\b${symbol}\\b" tests | wc -l`; - const testCount = parseInt(execSync(testCmd, { encoding: 'utf-8' }).trim()); - if (testCount === 0) { - console.log(`${fullPath}: ${symbol}`); - } - } catch(e) { - console.log(`${fullPath}: ${symbol}`); - } - } - } - } - } -} - -console.log("Scanning for unused exports..."); -scanDir('js'); diff --git a/tests/admin-list-store.test.mjs b/tests/admin-list-store.test.mjs index e8309d15..5cb39353 100644 --- a/tests/admin-list-store.test.mjs +++ b/tests/admin-list-store.test.mjs @@ -11,8 +11,6 @@ if (!globalThis.window.NostrTools) { globalThis.window.NostrTools = {}; } -globalThis.__BITVID_DEV_MODE_OVERRIDE__ = true; - const mockNip19 = { decode(value) { if (typeof value !== "string") {