mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
chore(studio): remove all console.* calls from studio package (#1691)
* chore(studio): remove all console.* calls from studio package * chore(studio): address review — remove dead stubs, restore consent notice - Delete empty if-blocks left after console removal (snapTargetCollection, Player asset-poll, useTimelineSyncCallbacks 5s probe, useGestureRecording dev guard + now-unused isDevBuild) and the stale "surface in dev" comment. - Drop the dangling no-console pragma + dead duplicate-id branch in sourcePatcher. - Restore the one-time telemetry consent disclosure in showNoticeOnce (kept behind a pragma — it is a user-facing notice, not debug noise). - Remove the missed timelineIcons console.warn while preserving the `tag || "div"` null-safety fallback. - Route caption auto-save failures (a data-loss path) through telemetry instead of swallowing silently. - Restore the accidentally-clobbered css-var-fonts output.mp4 fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ae8b94c518
commit
adb40321d6
+4
-3
@@ -6,9 +6,10 @@ pre-commit:
|
|||||||
run: bunx oxlint --no-error-on-unmatched-pattern {staged_files}
|
run: bunx oxlint --no-error-on-unmatched-pattern {staged_files}
|
||||||
format:
|
format:
|
||||||
glob: "*.{js,jsx,ts,tsx,json,md,yaml,yml}"
|
glob: "*.{js,jsx,ts,tsx,json,md,yaml,yml}"
|
||||||
# --no-error-on-unmatched-pattern: don't fail when staged files all
|
# Auto-format and re-stage so the committed snapshot is always formatted.
|
||||||
# fall under .prettierignore (e.g. docs-only changes to docs/docs.json).
|
# Replaces --check which only reports — that left unformatted files in
|
||||||
run: bunx oxfmt --check --no-error-on-unmatched-pattern {staged_files}
|
# commits when the hook ran after the amend snapshot was taken.
|
||||||
|
run: bunx oxfmt --no-error-on-unmatched-pattern {staged_files} && git add {staged_files}
|
||||||
typecheck:
|
typecheck:
|
||||||
glob: "*.{ts,tsx}"
|
glob: "*.{ts,tsx}"
|
||||||
run: cd packages/core && bunx tsc --noEmit && cd ../studio && bunx tsc --noEmit
|
run: cd packages/core && bunx tsc --noEmit && cd ../studio && bunx tsc --noEmit
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { useCaptionStore } from "../store";
|
import { useCaptionStore } from "../store";
|
||||||
import { useMountEffect } from "../../hooks/useMountEffect";
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
||||||
|
import { trackEvent } from "../../telemetry/client";
|
||||||
import type { CaptionStyle } from "../types";
|
import type { CaptionStyle } from "../types";
|
||||||
|
|
||||||
interface CaptionOverrideEntry {
|
interface CaptionOverrideEntry {
|
||||||
@@ -78,7 +79,11 @@ export function useCaptionSync(projectId: string | null) {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "text/plain" },
|
headers: { "Content-Type": "text/plain" },
|
||||||
body: JSON.stringify(overrides, null, 2),
|
body: JSON.stringify(overrides, null, 2),
|
||||||
}).catch((err) => console.warn("[captions] auto-save failed:", err));
|
}).catch((error: unknown) => {
|
||||||
|
// Caption auto-save is a data-loss path; surface failures via telemetry
|
||||||
|
// so a silently-dropped edit isn't invisible (no console in studio).
|
||||||
|
trackEvent("studio_caption_autosave_failed", { error: String(error) });
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Auto-save on model changes with 800ms debounce
|
// Auto-save on model changes with 800ms debounce
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface BlockParamsPanelProps {
|
|||||||
export const BlockParamsPanel = memo(function BlockParamsPanel({
|
export const BlockParamsPanel = memo(function BlockParamsPanel({
|
||||||
blockTitle,
|
blockTitle,
|
||||||
params,
|
params,
|
||||||
compositionPath,
|
compositionPath: _compositionPath,
|
||||||
onClose,
|
onClose,
|
||||||
}: BlockParamsPanelProps) {
|
}: BlockParamsPanelProps) {
|
||||||
const [values, setValues] = useState<Record<string, string>>(() => {
|
const [values, setValues] = useState<Record<string, string>>(() => {
|
||||||
@@ -23,13 +23,9 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
|
|||||||
return initial;
|
return initial;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback((key: string, value: string) => {
|
||||||
(key: string, value: string) => {
|
setValues((prev) => ({ ...prev, [key]: value }));
|
||||||
setValues((prev) => ({ ...prev, [key]: value }));
|
}, []);
|
||||||
console.log(`[BlockParams] ${compositionPath} ${key}: ${value}`);
|
|
||||||
},
|
|
||||||
[compositionPath],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
|
|||||||
@@ -100,11 +100,6 @@ export function collectSnapContext(input: {
|
|||||||
|
|
||||||
const MAX_SNAP_TARGETS = 80;
|
const MAX_SNAP_TARGETS = 80;
|
||||||
const elements = collectVisibleElements(root, input.excludeElements, MAX_SNAP_TARGETS);
|
const elements = collectVisibleElements(root, input.excludeElements, MAX_SNAP_TARGETS);
|
||||||
if (elements.length >= MAX_SNAP_TARGETS) {
|
|
||||||
console.warn(
|
|
||||||
`[snap] Target cap reached (${MAX_SNAP_TARGETS}). Elements beyond this limit are excluded from snap alignment.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries: Array<{
|
const entries: Array<{
|
||||||
rect: { left: number; top: number; width: number; height: number };
|
rect: { left: number; top: number; width: number; height: number };
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export function safeParseManifest(html: string): SlideshowManifest {
|
|||||||
try {
|
try {
|
||||||
return parseSlideshowManifest(html) ?? { slides: [] };
|
return parseSlideshowManifest(html) ?? { slides: [] };
|
||||||
} catch {
|
} catch {
|
||||||
console.warn("[SlideshowPanel] Failed to parse slideshow manifest; using empty manifest");
|
|
||||||
return { slides: [] };
|
return { slides: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,8 +165,7 @@ async function commitFlatViaKeyframes(
|
|||||||
if (Number.isFinite(v)) resolvedFromValues[key] = roundTo3(v);
|
if (Number.isFinite(v)) resolvedFromValues[key] = roundTo3(v);
|
||||||
}
|
}
|
||||||
mainTl.seek(ct);
|
mainTl.seek(ct);
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.warn("[gsap-drag] start-value read failed; using identity from values", err);
|
|
||||||
for (const key of Object.keys(resolvedFromValues)) delete resolvedFromValues[key];
|
for (const key of Object.keys(resolvedFromValues)) delete resolvedFromValues[key];
|
||||||
} finally {
|
} finally {
|
||||||
if (Object.keys(draggedValues).length > 0) gsapLib.set(el, draggedValues);
|
if (Object.keys(draggedValues).length > 0) gsapLib.set(el, draggedValues);
|
||||||
|
|||||||
@@ -115,12 +115,7 @@ export function readAllAnimatedProperties(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {}
|
||||||
console.warn(
|
|
||||||
"Cross-tween guard failed — baseline capture may include values from other tweens",
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const p of propKeys) otherTweenProps.delete(p);
|
for (const p of propKeys) otherTweenProps.delete(p);
|
||||||
|
|
||||||
// Tier 1: Transform + visual properties with universal CSS defaults.
|
// Tier 1: Transform + visual properties with universal CSS defaults.
|
||||||
|
|||||||
@@ -224,10 +224,6 @@ export function useDomEditCommits({
|
|||||||
target_source_file: selection.sourceFile ?? undefined,
|
target_source_file: selection.sourceFile ?? undefined,
|
||||||
composition: activeCompPath ?? undefined,
|
composition: activeCompPath ?? undefined,
|
||||||
});
|
});
|
||||||
console.warn(
|
|
||||||
`[studio] Element not found in source: ${targetKey}. ` +
|
|
||||||
"This element may be generated at runtime and cannot be persisted.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -126,9 +126,7 @@ export function useDomEditTextCommits({
|
|||||||
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
? (html, sourceFile) => ensureImportedFontFace(html, importedFont, sourceFile)
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn("[Studio] Style persist failed:", err instanceof Error ? err.message : err);
|
|
||||||
}
|
|
||||||
refreshDomEditSelectionFromPreview(domEditSelection);
|
refreshDomEditSelectionFromPreview(domEditSelection);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -162,9 +160,7 @@ export function useDomEditTextCommits({
|
|||||||
coalesceKey: `${options.coalescePrefix}:${attr}:${getDomEditTargetKey(domEditSelection)}`,
|
coalesceKey: `${options.coalescePrefix}:${attr}:${getDomEditTargetKey(domEditSelection)}`,
|
||||||
skipRefresh: options.skipRefresh,
|
skipRefresh: options.skipRefresh,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn(options.warningMessage, err instanceof Error ? err.message : err);
|
|
||||||
}
|
|
||||||
if (options.refreshAfter) {
|
if (options.refreshAfter) {
|
||||||
refreshDomEditSelectionFromPreview(domEditSelection);
|
refreshDomEditSelectionFromPreview(domEditSelection);
|
||||||
}
|
}
|
||||||
@@ -224,12 +220,7 @@ export function useDomEditTextCommits({
|
|||||||
coalesceKey: `html-attr:${attr}:${getDomEditTargetKey(domEditSelection)}`,
|
coalesceKey: `html-attr:${attr}:${getDomEditTargetKey(domEditSelection)}`,
|
||||||
skipRefresh: false,
|
skipRefresh: false,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn(
|
|
||||||
"[Studio] HTML attribute persist failed:",
|
|
||||||
err instanceof Error ? err.message : err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
refreshDomEditSelectionFromPreview(domEditSelection);
|
refreshDomEditSelectionFromPreview(domEditSelection);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { usePlayerStore, liveTime } from "../player/store/playerStore";
|
import { usePlayerStore, liveTime } from "../player/store/playerStore";
|
||||||
|
|
||||||
// `import.meta.env` may be undefined in non-Vite bundlers (Next.js Turbopack),
|
|
||||||
// so guard the access like the telemetry client does.
|
|
||||||
function isDevBuild(): boolean {
|
|
||||||
try {
|
|
||||||
return import.meta.env.DEV === true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GestureSample {
|
export interface GestureSample {
|
||||||
time: number;
|
time: number;
|
||||||
properties: Record<string, number>;
|
properties: Record<string, number>;
|
||||||
@@ -385,13 +375,9 @@ export function useGestureRecording() {
|
|||||||
if (r.runtime) {
|
if (r.runtime) {
|
||||||
try {
|
try {
|
||||||
applyRuntimePreview(r.runtime, time, properties);
|
applyRuntimePreview(r.runtime, time, properties);
|
||||||
} catch (err) {
|
} catch {
|
||||||
// Preview failed — disable it for the rest of the gesture (recording
|
// Preview failed — disable it for the rest of the gesture (recording
|
||||||
// continues). Surface in dev so a dead preview isn't silent; `r.runtime`
|
// continues). `r.runtime` is nulled so we don't retry on every frame.
|
||||||
// is nulled below so this warns at most once per gesture.
|
|
||||||
if (isDevBuild()) {
|
|
||||||
console.warn("[GR] live preview disabled — runtime threw:", err);
|
|
||||||
}
|
|
||||||
r.runtime = null;
|
r.runtime = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,11 +252,6 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|||||||
if (assetPollRef.current) clearInterval(assetPollRef.current);
|
if (assetPollRef.current) clearInterval(assetPollRef.current);
|
||||||
assetPollRef.current = null;
|
assetPollRef.current = null;
|
||||||
setAssetsLoading(false);
|
setAssetsLoading(false);
|
||||||
if (lastUnloaded) {
|
|
||||||
console.debug(
|
|
||||||
"[Player] Asset-loading overlay timed out after 10s; hiding anyway. Check network or asset integrity.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const ICONS: Record<string, ReactNode> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getTrackStyle(tag: string): TrackVisualStyle {
|
export function getTrackStyle(tag: string): TrackVisualStyle {
|
||||||
if (!tag) console.warn("[Timeline] getTrackStyle received empty tag, defaulting to div");
|
// Defensive: callers may pass an empty/undefined tag; fall back to "div".
|
||||||
const safeTag = tag || "div";
|
const safeTag = tag || "div";
|
||||||
const trackStyle = getTimelineTrackStyle(safeTag);
|
const trackStyle = getTimelineTrackStyle(safeTag);
|
||||||
const normalized = safeTag.toLowerCase();
|
const normalized = safeTag.toLowerCase();
|
||||||
|
|||||||
@@ -199,8 +199,7 @@ export function useTimelinePlayer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return bestAdapter;
|
return bestAdapter;
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.warn("[useTimelinePlayer] Could not get playback adapter (cross-origin)", err);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -264,9 +263,7 @@ export function useTimelinePlayer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn("[useTimelinePlayer] Could not set playback rate (cross-origin)", err);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
const applyPreviewAudioState = useCallback((playbackRateOverride?: number) => {
|
const applyPreviewAudioState = useCallback((playbackRateOverride?: number) => {
|
||||||
const { audioMuted, playbackRate } = usePlayerStore.getState();
|
const { audioMuted, playbackRate } = usePlayerStore.getState();
|
||||||
@@ -506,9 +503,7 @@ export function useTimelinePlayer() {
|
|||||||
if (msSinceTimeline > 500) {
|
if (msSinceTimeline > 500) {
|
||||||
enrichMissingCompositionsRef.current();
|
enrichMissingCompositionsRef.current();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn("[useTimelinePlayer] Could not read clip manifest from iframe", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (data?.source === "hf-preview" && data?.type === "timeline" && Array.isArray(data.clips)) {
|
if (data?.source === "hf-preview" && data?.type === "timeline" && Array.isArray(data.clips)) {
|
||||||
lastTimelineMessageRef.current = Date.now();
|
lastTimelineMessageRef.current = Date.now();
|
||||||
@@ -524,12 +519,7 @@ export function useTimelinePlayer() {
|
|||||||
syncTimelineElements(els);
|
syncTimelineElements(els);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn(
|
|
||||||
"[useTimelinePlayer] Could not read timeline elements on navigate (cross-origin)",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -164,9 +164,7 @@ export function useTimelineSyncCallbacks({
|
|||||||
const dedupedMissing = missing.filter((m) => !finalIds.has(m.id));
|
const dedupedMissing = missing.filter((m) => !finalIds.has(m.id));
|
||||||
syncTimelineElements([...updatedEls, ...dedupedMissing]);
|
syncTimelineElements([...updatedEls, ...dedupedMissing]);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn("[useTimelinePlayer] enrichMissingCompositions failed", err);
|
|
||||||
}
|
|
||||||
}, [iframeRef, syncTimelineElements]);
|
}, [iframeRef, syncTimelineElements]);
|
||||||
|
|
||||||
const initializeAdapter = useCallback(() => {
|
const initializeAdapter = useCallback(() => {
|
||||||
@@ -241,9 +239,7 @@ export function useTimelineSyncCallbacks({
|
|||||||
if (fallbackElement) syncTimelineElements([fallbackElement]);
|
if (fallbackElement) syncTimelineElements([fallbackElement]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn("[useTimelinePlayer] Could not read timeline elements from iframe", err);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}, [
|
}, [
|
||||||
getAdapter,
|
getAdapter,
|
||||||
@@ -295,9 +291,6 @@ export function useTimelineSyncCallbacks({
|
|||||||
probeIntervalRef.current = setTimeout(() => {
|
probeIntervalRef.current = setTimeout(() => {
|
||||||
if (!settled) {
|
if (!settled) {
|
||||||
trySettle();
|
trySettle();
|
||||||
if (!settled) {
|
|
||||||
console.warn("[useTimelinePlayer] Runtime did not signal readiness within 5s");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
window.removeEventListener("message", onMessage);
|
window.removeEventListener("message", onMessage);
|
||||||
}, 5000) as unknown as ReturnType<typeof setInterval>;
|
}, 5000) as unknown as ReturnType<typeof setInterval>;
|
||||||
|
|||||||
@@ -121,9 +121,7 @@ export function setPreviewMediaMuted(iframe: HTMLIFrameElement | null, muted: bo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
postPreviewControl(iframe, "set-muted", { muted });
|
postPreviewControl(iframe, "set-muted", { muted });
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn("[useTimelinePlayer] Failed to set preview media mute state", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setPreviewPlaybackRate(
|
export function setPreviewPlaybackRate(
|
||||||
@@ -139,9 +137,7 @@ export function setPreviewPlaybackRate(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
postPreviewControl(iframe, "set-playback-rate", { playbackRate: rate });
|
postPreviewControl(iframe, "set-playback-rate", { playbackRate: rate });
|
||||||
} catch (err) {
|
} catch {}
|
||||||
console.warn("[useTimelinePlayer] Failed to set preview playback rate", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ function send(url: string, payload: string): void {
|
|||||||
function showNoticeOnce(): void {
|
function showNoticeOnce(): void {
|
||||||
if (hasShownNotice()) return;
|
if (hasShownNotice()) return;
|
||||||
markNoticeShown();
|
markNoticeShown();
|
||||||
|
// Intentional one-time consent disclosure (not debug noise): tells users
|
||||||
|
// anonymous analytics are on and how to opt out. Kept behind a pragma.
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.info(
|
console.info(
|
||||||
"%c[HyperFrames]%c Anonymous studio usage analytics enabled. " +
|
"%c[HyperFrames]%c Anonymous studio usage analytics enabled. " +
|
||||||
|
|||||||
@@ -3,14 +3,7 @@
|
|||||||
// `window.__hfDebug = true` in the console. Single `[hf-edit:<scope>]` prefix so
|
// `window.__hfDebug = true` in the console. Single `[hf-edit:<scope>]` prefix so
|
||||||
// the whole edit pipeline is greppable. Fires only at commit boundaries (user
|
// the whole edit pipeline is greppable. Fires only at commit boundaries (user
|
||||||
// actions), never in render/raf loops, so it doesn't spam.
|
// actions), never in render/raf loops, so it doesn't spam.
|
||||||
export function editLog(scope: string, ...args: unknown[]): void {
|
export function editLog(_scope: string, ..._args: unknown[]): void {
|
||||||
if (typeof window === "undefined") return;
|
// ponytail: body removed — all console.* stripped from studio.
|
||||||
const w = window as unknown as { __hfDebug?: boolean };
|
// Restore with: console.log(`[hf-edit:${_scope}]`, ..._args);
|
||||||
if (!import.meta.env.DEV && !w.__hfDebug) return;
|
|
||||||
// Stringify object args so the console prints their contents inline (`{x:1}`)
|
|
||||||
// instead of a collapsed `Object` — keeps the edit trail greppable/copyable.
|
|
||||||
const parts = args.map((a) =>
|
|
||||||
typeof a === "object" && a !== null ? JSON.stringify(a) : String(a),
|
|
||||||
);
|
|
||||||
console.debug(`[hf-edit:${scope}]`, ...parts);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ export async function executeOptimistic<T>(options: OptimisticUpdateOptions<T>):
|
|||||||
const snapshot = options.apply();
|
const snapshot = options.apply();
|
||||||
try {
|
try {
|
||||||
await options.persist();
|
await options.persist();
|
||||||
} catch (error) {
|
} catch {
|
||||||
options.rollback(snapshot);
|
options.rollback(snapshot);
|
||||||
console.warn("[optimistic] Mutation failed, rolled back:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,16 +239,6 @@ function execDataAttrPattern(html: string, attr: string, value: string): TagMatc
|
|||||||
const pattern = new RegExp(`(<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\2[^>]*)>`, "i");
|
const pattern = new RegExp(`(<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\2[^>]*)>`, "i");
|
||||||
const match = pattern.exec(html);
|
const match = pattern.exec(html);
|
||||||
if (match?.index == null) return null;
|
if (match?.index == null) return null;
|
||||||
// Defensive: a second exact match means a duplicate id/attr in the source
|
|
||||||
// (id drift). Don't silently patch the first while leaving the other stale —
|
|
||||||
// surface it. By the mint contract this should never fire.
|
|
||||||
const all = html.match(new RegExp(`<[^>]*\\b${attr}=(["'])${escapeRegex(value)}\\1[^>]*>`, "gi"));
|
|
||||||
if (all && all.length > 1) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(
|
|
||||||
`sourcePatcher: ${attr}="${value}" matched ${all.length} elements; patching the first. ids/attrs must be unique per document.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { tag: match[1], start: match.index, end: match.index + match[1].length };
|
return { tag: match[1], start: match.index, end: match.index + match[1].length };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user