@accelerated-agency/visual-editor 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,6 +5,11 @@ import { persist } from 'zustand/middleware';
5
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
6
  import { HexColorPicker } from 'react-colorful';
7
7
 
8
+ var PERSIST_NAME_PREFIX = "conversion-editor-variations-v3-";
9
+ function variationsPersistStorageName(experimentId) {
10
+ const id = experimentId?.trim();
11
+ return `${PERSIST_NAME_PREFIX}${id && id.length > 0 ? id : "standalone"}`;
12
+ }
8
13
  var VARIATION_COLORS = [
9
14
  "#0084D1",
10
15
  "#F54A00",
@@ -77,16 +82,12 @@ var useVariationsStore = create()(
77
82
  }));
78
83
  },
79
84
  setActiveVariation: (id) => set({ activeVariationId: id }),
80
- addMutationToActive: (mutation) => set((s) => {
81
- console.log("addMutationToActive", s.activeVariationId, mutation);
82
- return {
83
- variations: s.variations.map(
84
- (v) => v.id === s.activeVariationId ? { ...v, mutations: [...v.mutations, mutation] } : v
85
- ),
86
- // Clear redo stack for active variation on new mutation
87
- redoStacks: { ...s.redoStacks, [s.activeVariationId]: [] }
88
- };
89
- }),
85
+ addMutationToActive: (mutation) => set((s) => ({
86
+ variations: s.variations.map(
87
+ (v) => v.id === s.activeVariationId ? { ...v, mutations: [...v.mutations, mutation] } : v
88
+ ),
89
+ redoStacks: { ...s.redoStacks, [s.activeVariationId]: [] }
90
+ })),
90
91
  removeLastMutationFromActive: () => set((s) => {
91
92
  const active = s.variations.find((v) => v.id === s.activeVariationId);
92
93
  const removed = active?.mutations[active.mutations.length - 1];
@@ -149,7 +150,8 @@ var useVariationsStore = create()(
149
150
  }
150
151
  }),
151
152
  {
152
- name: "conversion-editor-variations",
153
+ name: variationsPersistStorageName(void 0),
154
+ skipHydration: true,
153
155
  partialize: (state) => ({
154
156
  variations: state.variations,
155
157
  activeVariationId: state.activeVariationId
@@ -157,6 +159,15 @@ var useVariationsStore = create()(
157
159
  }
158
160
  )
159
161
  );
162
+ async function hydrateVariationsFromStorage(experimentId) {
163
+ useVariationsStore.persist.setOptions({
164
+ name: variationsPersistStorageName(experimentId)
165
+ });
166
+ try {
167
+ await useVariationsStore.persist.rehydrate();
168
+ } catch {
169
+ }
170
+ }
160
171
  function TopBar({
161
172
  connectionStatus,
162
173
  onLoadUrl,
@@ -1832,8 +1843,10 @@ function ElementIcon({ tag }) {
1832
1843
  return /* @__PURE__ */ jsx("span", { className: "shrink-0 w-5 h-5 rounded flex items-center justify-center", style: { backgroundColor: bgColor }, children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx("rect", { x: "1.5", y: "1.5", width: "9", height: "9", rx: "1.5", stroke: iconColor, strokeWidth: "1.2" }) }) });
1833
1844
  }
1834
1845
  var CHANNEL = "conversion-editor";
1846
+ var IFRAME_LOAD_GUARD_MS = 13e4;
1835
1847
  function IframeCanvas({ url, password, proxyBaseUrl = "", onBridgeReady, onPong }) {
1836
1848
  const iframeElRef = useRef(null);
1849
+ const loadGuardRef = useRef(null);
1837
1850
  const setSelectedElement = useMutationsStore((s) => s.setSelectedElement);
1838
1851
  const addMutationToActive = useVariationsStore((s) => s.addMutationToActive);
1839
1852
  const [loading, setLoading] = useState(false);
@@ -1876,8 +1889,24 @@ function IframeCanvas({ url, password, proxyBaseUrl = "", onBridgeReady, onPong
1876
1889
  return () => window.removeEventListener("message", handleMessage);
1877
1890
  }, [handleMessage]);
1878
1891
  useEffect(() => {
1879
- if (url) setLoading(true);
1892
+ if (!url) return;
1893
+ setLoading(true);
1894
+ if (loadGuardRef.current) clearTimeout(loadGuardRef.current);
1895
+ loadGuardRef.current = setTimeout(() => {
1896
+ loadGuardRef.current = null;
1897
+ setLoading(false);
1898
+ }, IFRAME_LOAD_GUARD_MS);
1899
+ return () => {
1900
+ if (loadGuardRef.current) clearTimeout(loadGuardRef.current);
1901
+ };
1880
1902
  }, [url]);
1903
+ const clearLoadGuard = useCallback(() => {
1904
+ if (loadGuardRef.current) {
1905
+ clearTimeout(loadGuardRef.current);
1906
+ loadGuardRef.current = null;
1907
+ }
1908
+ setLoading(false);
1909
+ }, []);
1881
1910
  let resolvedUrl;
1882
1911
  if (url.toLowerCase() === "test") {
1883
1912
  resolvedUrl = "/test";
@@ -1908,7 +1937,8 @@ function IframeCanvas({ url, password, proxyBaseUrl = "", onBridgeReady, onPong
1908
1937
  src: resolvedUrl,
1909
1938
  className: "w-full h-full border-0",
1910
1939
  sandbox: "allow-scripts allow-same-origin allow-forms allow-popups",
1911
- onLoad: () => setLoading(false)
1940
+ onLoad: clearLoadGuard,
1941
+ onError: clearLoadGuard
1912
1942
  }
1913
1943
  )
1914
1944
  ] });
@@ -4059,6 +4089,9 @@ function sendToPlatform(type, payload) {
4059
4089
  data: { channel: PLATFORM_CHANNEL, type, payload }
4060
4090
  }));
4061
4091
  }
4092
+ function experimentIframeContextKey(exp) {
4093
+ return [exp.experimentId ?? "", exp.pageUrl ?? "", exp.editorPassword ?? ""].join("");
4094
+ }
4062
4095
  function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4063
4096
  const [url, setUrl] = useState("");
4064
4097
  const [password, setPassword] = useState("");
@@ -4071,7 +4104,8 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4071
4104
  );
4072
4105
  const [experimentData, setExperimentData] = useState(null);
4073
4106
  const experimentDataRef = useRef(null);
4074
- const lastInitialExperimentKeyRef = useRef("");
4107
+ const lastAppliedRunKeyRef = useRef("");
4108
+ const iframeContextKeyRef = useRef("");
4075
4109
  useEffect(() => {
4076
4110
  experimentDataRef.current = experimentData;
4077
4111
  }, [experimentData]);
@@ -4089,7 +4123,6 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4089
4123
  const clearAll = useVariationsStore((s) => s.clearAll);
4090
4124
  const setSelectedElement = useMutationsStore((s) => s.setSelectedElement);
4091
4125
  const selectedElement = useMutationsStore((s) => s.selectedElement);
4092
- const activeVariationId = useVariationsStore((s) => s.activeVariationId);
4093
4126
  const variations = useVariationsStore((s) => s.variations);
4094
4127
  const removeLastMutationFromActive = useVariationsStore((s) => s.removeLastMutationFromActive);
4095
4128
  const removeMutationsForSelector = useVariationsStore((s) => s.removeMutationsForSelector);
@@ -4140,7 +4173,6 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4140
4173
  }, [embedded, experimentData, toast, buildPlatformVariations]);
4141
4174
  const pingIntervalRef = useRef(null);
4142
4175
  const pongTimeoutRef = useRef(null);
4143
- const syncDebounceRef = useRef(null);
4144
4176
  const stopHeartbeat = useCallback(() => {
4145
4177
  if (pingIntervalRef.current) {
4146
4178
  clearInterval(pingIntervalRef.current);
@@ -4174,15 +4206,24 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4174
4206
  },
4175
4207
  [stopHeartbeat, clearAll, setSelectedElement]
4176
4208
  );
4209
+ useEffect(() => {
4210
+ if (initialExperiment) return;
4211
+ let cancelled = false;
4212
+ void (async () => {
4213
+ await hydrateVariationsFromStorage(void 0);
4214
+ if (cancelled) return;
4215
+ })();
4216
+ return () => {
4217
+ cancelled = true;
4218
+ };
4219
+ }, [initialExperiment]);
4177
4220
  useEffect(() => {
4178
4221
  if (!initialExperiment) return;
4179
- const key = JSON.stringify(initialExperiment);
4180
- if (lastInitialExperimentKeyRef.current === key) return;
4181
- lastInitialExperimentKeyRef.current = key;
4182
- setExperimentData(initialExperiment);
4183
- if (initialExperiment.pageUrl) {
4184
- handleLoadUrl(initialExperiment.pageUrl, initialExperiment.editorPassword || void 0);
4185
- }
4222
+ const runKey = JSON.stringify(initialExperiment);
4223
+ if (lastAppliedRunKeyRef.current === runKey) return;
4224
+ const ctx = experimentIframeContextKey(initialExperiment);
4225
+ const sameIframeContext = iframeContextKeyRef.current !== "" && iframeContextKeyRef.current === ctx;
4226
+ let cancelled = false;
4186
4227
  const sourceVariations = Array.isArray(initialExperiment.variations) ? initialExperiment.variations : [];
4187
4228
  const editorVariations = sourceVariations.map((v, idx) => {
4188
4229
  let mutations = [];
@@ -4202,7 +4243,31 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4202
4243
  mutations
4203
4244
  };
4204
4245
  });
4205
- loadExperimentVariations(editorVariations);
4246
+ setExperimentData(initialExperiment);
4247
+ if (sameIframeContext) {
4248
+ void (async () => {
4249
+ await hydrateVariationsFromStorage(initialExperiment.experimentId);
4250
+ if (cancelled) return;
4251
+ loadExperimentVariations(editorVariations);
4252
+ lastAppliedRunKeyRef.current = runKey;
4253
+ })();
4254
+ return () => {
4255
+ cancelled = true;
4256
+ };
4257
+ }
4258
+ if (initialExperiment.pageUrl) {
4259
+ handleLoadUrl(initialExperiment.pageUrl, initialExperiment.editorPassword || void 0);
4260
+ iframeContextKeyRef.current = ctx;
4261
+ }
4262
+ void (async () => {
4263
+ await hydrateVariationsFromStorage(initialExperiment.experimentId);
4264
+ if (cancelled) return;
4265
+ loadExperimentVariations(editorVariations);
4266
+ lastAppliedRunKeyRef.current = runKey;
4267
+ })();
4268
+ return () => {
4269
+ cancelled = true;
4270
+ };
4206
4271
  }, [initialExperiment, handleLoadUrl, loadExperimentVariations]);
4207
4272
  const handleBridgeReady = useCallback(() => {
4208
4273
  setConnectionStatus("connected");
@@ -4232,33 +4297,44 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4232
4297
  switch (msg.type) {
4233
4298
  case "load-experiment": {
4234
4299
  const data = msg.payload;
4235
- setExperimentData(data);
4236
4300
  experimentDataRef.current = data;
4237
- if (data.pageUrl && !msg.payload?.skipUrlReload) {
4238
- handleLoadUrl(data.pageUrl, data.editorPassword || void 0);
4239
- }
4240
- if (Array.isArray(data.variations)) {
4241
- const editorVariations = data.variations.map((v, idx) => {
4242
- let mutations = [];
4243
- try {
4244
- const chainSets = JSON.parse(v.changesets || "[]");
4245
- if (Array.isArray(chainSets) && chainSets.length > 0) {
4246
- mutations = convertChainSetsToMutations(chainSets);
4247
- }
4248
- } catch {
4301
+ const editorVariations = Array.isArray(data.variations) ? data.variations.map((v, idx) => {
4302
+ let mutations = [];
4303
+ try {
4304
+ const chainSets = JSON.parse(v.changesets || "[]");
4305
+ if (Array.isArray(chainSets) && chainSets.length > 0) {
4306
+ mutations = convertChainSetsToMutations(chainSets);
4249
4307
  }
4250
- return {
4251
- id: v._id || `v_${idx}`,
4252
- platformIid: v.iid,
4253
- name: v.name,
4254
- isControl: v.baseline || false,
4255
- traffic_allocation: v.traffic_allocation,
4256
- mutations
4257
- };
4258
- });
4259
- loadExperimentVariations(editorVariations);
4308
+ } catch {
4309
+ }
4310
+ return {
4311
+ id: v._id || `v_${idx}`,
4312
+ platformIid: v.iid,
4313
+ name: v.name,
4314
+ isControl: v.baseline || false,
4315
+ traffic_allocation: v.traffic_allocation,
4316
+ mutations
4317
+ };
4318
+ }) : [];
4319
+ setExperimentData(data);
4320
+ const ctx = experimentIframeContextKey(data);
4321
+ let reloadedIframe = false;
4322
+ if (data.pageUrl && !data.skipUrlReload) {
4323
+ if (iframeContextKeyRef.current !== ctx) {
4324
+ handleLoadUrl(data.pageUrl, data.editorPassword || void 0);
4325
+ iframeContextKeyRef.current = ctx;
4326
+ reloadedIframe = true;
4327
+ }
4260
4328
  }
4261
- toast(`Loaded experiment: ${data.name || "Untitled"}`, "info");
4329
+ void (async () => {
4330
+ await hydrateVariationsFromStorage(data.experimentId);
4331
+ if (Array.isArray(data.variations)) {
4332
+ loadExperimentVariations(editorVariations);
4333
+ }
4334
+ if (reloadedIframe) {
4335
+ toast(`Loaded experiment: ${data.name || "Untitled"}`, "info");
4336
+ }
4337
+ })();
4262
4338
  break;
4263
4339
  }
4264
4340
  case "request-save": {
@@ -4332,27 +4408,6 @@ function EditorShell({ initialExperiment, embeddedMode, proxyBaseUrl }) {
4332
4408
  useEffect(() => {
4333
4409
  sendToBridge({ type: "setMode", mode: interactionMode });
4334
4410
  }, [interactionMode]);
4335
- useEffect(() => {
4336
- if (syncDebounceRef.current) {
4337
- clearTimeout(syncDebounceRef.current);
4338
- syncDebounceRef.current = null;
4339
- }
4340
- if (connectionStatus !== "connected") return;
4341
- syncDebounceRef.current = setTimeout(() => {
4342
- const currentActiveMutations = useVariationsStore.getState().getActiveMutations();
4343
- sendToBridge({ type: "clearAllMutations" });
4344
- if (currentActiveMutations.length > 0) {
4345
- sendToBridge({ type: "applyMutationBatch", mutations: currentActiveMutations });
4346
- }
4347
- syncDebounceRef.current = null;
4348
- }, 350);
4349
- return () => {
4350
- if (syncDebounceRef.current) {
4351
- clearTimeout(syncDebounceRef.current);
4352
- syncDebounceRef.current = null;
4353
- }
4354
- };
4355
- }, [connectionStatus, activeVariationId, experimentData?.experimentId]);
4356
4411
  useEffect(() => {
4357
4412
  const handler = (e) => {
4358
4413
  const meta = e.metaKey || e.ctrlKey;
@@ -4555,7 +4610,7 @@ function PlatformVisualEditor({
4555
4610
  renderLoading,
4556
4611
  renderError
4557
4612
  }) {
4558
- console.log(experiment);
4613
+ console.log("experiment", experiment);
4559
4614
  const [editorReady, setEditorReady] = useState(false);
4560
4615
  const [dirty, setDirty] = useState(false);
4561
4616
  const dirtyRef = useRef(false);
@@ -4769,7 +4824,6 @@ function PlatformVisualEditorV2({
4769
4824
  renderLoading,
4770
4825
  renderError
4771
4826
  }) {
4772
- console.log(experiment);
4773
4827
  const iframeRef = useRef(null);
4774
4828
  const [editorReady, setEditorReady] = useState(false);
4775
4829
  const [dirty, setDirty] = useState(false);
@@ -4959,4 +5013,4 @@ function PlatformVisualEditorV2({
4959
5013
  ] });
4960
5014
  }
4961
5015
 
4962
- export { EditorShell, PlatformVisualEditor, PlatformVisualEditorV2, ToastProvider, useToast };
5016
+ export { EditorShell, PlatformVisualEditor, PlatformVisualEditorV2, ToastProvider, hydrateVariationsFromStorage, useToast, variationsPersistStorageName };
package/dist/vite.cjs CHANGED
@@ -1800,7 +1800,7 @@ function createVisualEditorMiddleware(options) {
1800
1800
  }
1801
1801
  }
1802
1802
  return async (req, res, next) => {
1803
- const pathname = (req.url || "").split("?")[0];
1803
+ let pathname = (req.url || "").split("?")[0];
1804
1804
  if (pathname === "/bridge.js") {
1805
1805
  res.removeHeader("X-Frame-Options");
1806
1806
  res.setHeader("Content-Type", "application/javascript; charset=utf-8");
@@ -1937,8 +1937,8 @@ function createVisualEditorMiddleware(options) {
1937
1937
  return;
1938
1938
  }
1939
1939
  if (pathname.startsWith("/api/proxy")) {
1940
- const rewritten = (req.url || "").replace("/api/proxy", "/api/conversion-proxy");
1941
- req.url = rewritten;
1940
+ req.url = (req.url || "").replace("/api/proxy", "/api/conversion-proxy");
1941
+ pathname = (req.url || "").split("?")[0];
1942
1942
  }
1943
1943
  if (pathname.startsWith("/api/conversion-proxy")) {
1944
1944
  try {
@@ -2001,17 +2001,39 @@ function createVisualEditorMiddleware(options) {
2001
2001
  }
2002
2002
  if (chunks.length > 0) requestBody = Buffer.concat(chunks);
2003
2003
  }
2004
- const upstream = await fetch(targetUrl, {
2005
- method,
2006
- headers: fetchHeaders,
2007
- body: requestBody ? Buffer.from(requestBody) : null,
2008
- redirect: "follow"
2009
- });
2004
+ const upstreamTimeoutMs = 12e4;
2005
+ const ac = new AbortController();
2006
+ const timeoutId = setTimeout(() => ac.abort(), upstreamTimeoutMs);
2007
+ let upstream;
2008
+ try {
2009
+ upstream = await fetch(targetUrl, {
2010
+ method,
2011
+ headers: fetchHeaders,
2012
+ body: requestBody ? Buffer.from(requestBody) : null,
2013
+ redirect: "follow",
2014
+ signal: ac.signal
2015
+ });
2016
+ } catch (fetchErr) {
2017
+ clearTimeout(timeoutId);
2018
+ const aborted = fetchErr?.name === "AbortError";
2019
+ res.statusCode = aborted ? 504 : 502;
2020
+ res.setHeader("Content-Type", "application/json");
2021
+ res.end(
2022
+ JSON.stringify({
2023
+ error: aborted ? `Upstream request timed out after ${upstreamTimeoutMs / 1e3}s` : fetchErr?.message || "Upstream fetch failed"
2024
+ })
2025
+ );
2026
+ return;
2027
+ }
2028
+ clearTimeout(timeoutId);
2010
2029
  const responseContentType = upstream.headers.get("content-type") || "";
2011
2030
  const isHtmlResponse = responseContentType.includes("text/html");
2031
+ const secFetchMode = (req.headers?.["sec-fetch-mode"] || "").toLowerCase();
2012
2032
  const secFetchDest = (req.headers?.["sec-fetch-dest"] || "").toLowerCase();
2013
- const isNavigationRequest = secFetchDest === "iframe" || secFetchDest === "document" || secFetchDest === "";
2014
- if (!isHtmlResponse || !isNavigationRequest) {
2033
+ const isLikelyDocumentNavigation = secFetchMode === "navigate" || secFetchDest === "iframe" || secFetchDest === "document" || secFetchDest === "nested-document" || secFetchDest === "frame";
2034
+ const isLikelyFetchOrXHR = secFetchDest === "empty" && (secFetchMode === "cors" || secFetchMode === "same-origin" || secFetchMode === "no-cors");
2035
+ const shouldInjectHtmlBridge = isHtmlResponse && (isLikelyDocumentNavigation || !isLikelyFetchOrXHR);
2036
+ if (!isHtmlResponse || !shouldInjectHtmlBridge) {
2015
2037
  const binary = Buffer.from(await upstream.arrayBuffer());
2016
2038
  res.statusCode = upstream.status;
2017
2039
  if (responseContentType) {
@@ -2044,7 +2066,7 @@ function createVisualEditorMiddleware(options) {
2044
2066
  html = html.replace("</head>", `${popupHideCss}
2045
2067
  </head>`);
2046
2068
  }
2047
- const runtimeProxyScript = `<script>(function(){try{var TARGET_ORIGIN=${JSON.stringify(origin)};var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}if(window.fetch){var _fetch=window.fetch.bind(window);window.fetch=function(input,init){try{var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();if(typeof input==="string"){input=toAbsoluteOriginUrl(input);}else if(input&&input.url){var next=toAbsoluteOriginUrl(input.url);if(next!==input.url){input=new Request(next,input);}}var after=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(after&&skipNestedProxyNetwork(after))return emptyJsonFetchResponse();}catch(_){}return _fetch(input,init);};}if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}}catch(_){}})();</script>`;
2069
+ const runtimeProxyScript = `<script>(function(){try{var TARGET_ORIGIN=${JSON.stringify(origin)};var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}if(window.fetch){var _fetch=window.fetch.bind(window);window.fetch=function(input,init){try{var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();if(typeof input==="string"){input=toAbsoluteOriginUrl(input);}else if(input&&input.url){var next=toAbsoluteOriginUrl(input.url);if(next!==input.url){input=new Request(next,input);}}var after=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(after&&skipNestedProxyNetwork(after))return emptyJsonFetchResponse();}catch(_){}return _fetch(input,init);};}if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}if(window.navigator&&typeof window.navigator.sendBeacon==="function"){var _beacon=window.navigator.sendBeacon.bind(window.navigator);window.navigator.sendBeacon=function(url,data){try{if(skipNestedProxyNetwork(String(url)))return true;}catch(_){}return _beacon(url,data);};}if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}}catch(_){}})();</script>`;
2048
2070
  if (html.includes("</head>")) {
2049
2071
  html = html.replace("</head>", `${runtimeProxyScript}
2050
2072
  </head>`);
package/dist/vite.js CHANGED
@@ -1792,7 +1792,7 @@ function createVisualEditorMiddleware(options) {
1792
1792
  }
1793
1793
  }
1794
1794
  return async (req, res, next) => {
1795
- const pathname = (req.url || "").split("?")[0];
1795
+ let pathname = (req.url || "").split("?")[0];
1796
1796
  if (pathname === "/bridge.js") {
1797
1797
  res.removeHeader("X-Frame-Options");
1798
1798
  res.setHeader("Content-Type", "application/javascript; charset=utf-8");
@@ -1929,8 +1929,8 @@ function createVisualEditorMiddleware(options) {
1929
1929
  return;
1930
1930
  }
1931
1931
  if (pathname.startsWith("/api/proxy")) {
1932
- const rewritten = (req.url || "").replace("/api/proxy", "/api/conversion-proxy");
1933
- req.url = rewritten;
1932
+ req.url = (req.url || "").replace("/api/proxy", "/api/conversion-proxy");
1933
+ pathname = (req.url || "").split("?")[0];
1934
1934
  }
1935
1935
  if (pathname.startsWith("/api/conversion-proxy")) {
1936
1936
  try {
@@ -1993,17 +1993,39 @@ function createVisualEditorMiddleware(options) {
1993
1993
  }
1994
1994
  if (chunks.length > 0) requestBody = Buffer.concat(chunks);
1995
1995
  }
1996
- const upstream = await fetch(targetUrl, {
1997
- method,
1998
- headers: fetchHeaders,
1999
- body: requestBody ? Buffer.from(requestBody) : null,
2000
- redirect: "follow"
2001
- });
1996
+ const upstreamTimeoutMs = 12e4;
1997
+ const ac = new AbortController();
1998
+ const timeoutId = setTimeout(() => ac.abort(), upstreamTimeoutMs);
1999
+ let upstream;
2000
+ try {
2001
+ upstream = await fetch(targetUrl, {
2002
+ method,
2003
+ headers: fetchHeaders,
2004
+ body: requestBody ? Buffer.from(requestBody) : null,
2005
+ redirect: "follow",
2006
+ signal: ac.signal
2007
+ });
2008
+ } catch (fetchErr) {
2009
+ clearTimeout(timeoutId);
2010
+ const aborted = fetchErr?.name === "AbortError";
2011
+ res.statusCode = aborted ? 504 : 502;
2012
+ res.setHeader("Content-Type", "application/json");
2013
+ res.end(
2014
+ JSON.stringify({
2015
+ error: aborted ? `Upstream request timed out after ${upstreamTimeoutMs / 1e3}s` : fetchErr?.message || "Upstream fetch failed"
2016
+ })
2017
+ );
2018
+ return;
2019
+ }
2020
+ clearTimeout(timeoutId);
2002
2021
  const responseContentType = upstream.headers.get("content-type") || "";
2003
2022
  const isHtmlResponse = responseContentType.includes("text/html");
2023
+ const secFetchMode = (req.headers?.["sec-fetch-mode"] || "").toLowerCase();
2004
2024
  const secFetchDest = (req.headers?.["sec-fetch-dest"] || "").toLowerCase();
2005
- const isNavigationRequest = secFetchDest === "iframe" || secFetchDest === "document" || secFetchDest === "";
2006
- if (!isHtmlResponse || !isNavigationRequest) {
2025
+ const isLikelyDocumentNavigation = secFetchMode === "navigate" || secFetchDest === "iframe" || secFetchDest === "document" || secFetchDest === "nested-document" || secFetchDest === "frame";
2026
+ const isLikelyFetchOrXHR = secFetchDest === "empty" && (secFetchMode === "cors" || secFetchMode === "same-origin" || secFetchMode === "no-cors");
2027
+ const shouldInjectHtmlBridge = isHtmlResponse && (isLikelyDocumentNavigation || !isLikelyFetchOrXHR);
2028
+ if (!isHtmlResponse || !shouldInjectHtmlBridge) {
2007
2029
  const binary = Buffer.from(await upstream.arrayBuffer());
2008
2030
  res.statusCode = upstream.status;
2009
2031
  if (responseContentType) {
@@ -2036,7 +2058,7 @@ function createVisualEditorMiddleware(options) {
2036
2058
  html = html.replace("</head>", `${popupHideCss}
2037
2059
  </head>`);
2038
2060
  }
2039
- const runtimeProxyScript = `<script>(function(){try{var TARGET_ORIGIN=${JSON.stringify(origin)};var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}if(window.fetch){var _fetch=window.fetch.bind(window);window.fetch=function(input,init){try{var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();if(typeof input==="string"){input=toAbsoluteOriginUrl(input);}else if(input&&input.url){var next=toAbsoluteOriginUrl(input.url);if(next!==input.url){input=new Request(next,input);}}var after=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(after&&skipNestedProxyNetwork(after))return emptyJsonFetchResponse();}catch(_){}return _fetch(input,init);};}if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}}catch(_){}})();</script>`;
2061
+ const runtimeProxyScript = `<script>(function(){try{var TARGET_ORIGIN=${JSON.stringify(origin)};var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}if(window.fetch){var _fetch=window.fetch.bind(window);window.fetch=function(input,init){try{var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();if(typeof input==="string"){input=toAbsoluteOriginUrl(input);}else if(input&&input.url){var next=toAbsoluteOriginUrl(input.url);if(next!==input.url){input=new Request(next,input);}}var after=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(after&&skipNestedProxyNetwork(after))return emptyJsonFetchResponse();}catch(_){}return _fetch(input,init);};}if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}if(window.navigator&&typeof window.navigator.sendBeacon==="function"){var _beacon=window.navigator.sendBeacon.bind(window.navigator);window.navigator.sendBeacon=function(url,data){try{if(skipNestedProxyNetwork(String(url)))return true;}catch(_){}return _beacon(url,data);};}if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}}catch(_){}})();</script>`;
2040
2062
  if (html.includes("</head>")) {
2041
2063
  html = html.replace("</head>", `${runtimeProxyScript}
2042
2064
  </head>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelerated-agency/visual-editor",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "description": "Conversion visual editor as a reusable React package",
6
6
  "type": "module",