@antigenic-oss/paint 0.1.0
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/LICENSE +178 -0
- package/NOTICE +4 -0
- package/README.md +180 -0
- package/bin/paint.js +266 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +19 -0
- package/package.json +81 -0
- package/postcss.config.mjs +8 -0
- package/public/dev-editor-inspector.js +1872 -0
- package/src/app/api/claude/analyze/route.ts +319 -0
- package/src/app/api/claude/apply/route.ts +185 -0
- package/src/app/api/claude/pick-folder/route.ts +64 -0
- package/src/app/api/claude/scan/route.ts +221 -0
- package/src/app/api/claude/status/route.ts +55 -0
- package/src/app/api/project/scan/route.ts +634 -0
- package/src/app/api/project-scan/css-variables/route.ts +238 -0
- package/src/app/api/project-scan/route.ts +40 -0
- package/src/app/api/project-scan/tailwind-config/route.ts +172 -0
- package/src/app/api/proxy/[[...path]]/route.ts +2400 -0
- package/src/app/docs/DocsClient.tsx +322 -0
- package/src/app/docs/layout.tsx +7 -0
- package/src/app/docs/page.tsx +855 -0
- package/src/app/globals.css +176 -0
- package/src/app/layout.tsx +19 -0
- package/src/app/page.tsx +46 -0
- package/src/bridge/api-handlers.ts +885 -0
- package/src/bridge/proxy-handler.ts +329 -0
- package/src/bridge/server.ts +113 -0
- package/src/components/BreakpointTabs.tsx +72 -0
- package/src/components/ChangeSummaryModal.tsx +267 -0
- package/src/components/ConnectModal.tsx +994 -0
- package/src/components/Editor.tsx +90 -0
- package/src/components/PageSelector.tsx +208 -0
- package/src/components/PreviewFrame.tsx +299 -0
- package/src/components/ProjectFolderBanner.tsx +91 -0
- package/src/components/ResponsiveToolbar.tsx +222 -0
- package/src/components/TargetSelector.tsx +243 -0
- package/src/components/TopBar.tsx +315 -0
- package/src/components/common/CollapsibleSection.tsx +36 -0
- package/src/components/common/ColorPicker.tsx +920 -0
- package/src/components/common/EditablePre.tsx +136 -0
- package/src/components/common/ErrorBoundary.tsx +65 -0
- package/src/components/common/ResizablePanel.tsx +83 -0
- package/src/components/common/ScanAnimation.tsx +76 -0
- package/src/components/common/ToastContainer.tsx +97 -0
- package/src/components/common/UnitInput.tsx +77 -0
- package/src/components/common/VariableColorPicker.tsx +622 -0
- package/src/components/left-panel/AddElementPanel.tsx +237 -0
- package/src/components/left-panel/ComponentsPanel.tsx +609 -0
- package/src/components/left-panel/IconSidebar.tsx +99 -0
- package/src/components/left-panel/LayerNode.tsx +874 -0
- package/src/components/left-panel/LayerSearch.tsx +23 -0
- package/src/components/left-panel/LayersPanel.tsx +52 -0
- package/src/components/left-panel/LeftPanel.tsx +122 -0
- package/src/components/left-panel/PagesPanel.tsx +114 -0
- package/src/components/left-panel/icons.tsx +162 -0
- package/src/components/left-panel/terminal/ScanOverlay.tsx +66 -0
- package/src/components/left-panel/terminal/TerminalPanel.tsx +217 -0
- package/src/components/right-panel/ElementLogBox.tsx +248 -0
- package/src/components/right-panel/PanelTabs.tsx +83 -0
- package/src/components/right-panel/RightPanel.tsx +41 -0
- package/src/components/right-panel/changes/AiScanResultPanel.tsx +285 -0
- package/src/components/right-panel/changes/ChangeEntry.tsx +59 -0
- package/src/components/right-panel/changes/ChangelogActions.tsx +105 -0
- package/src/components/right-panel/changes/ChangesPanel.tsx +1474 -0
- package/src/components/right-panel/claude/ApplyConfirmModal.tsx +376 -0
- package/src/components/right-panel/claude/ClaudeErrorState.tsx +125 -0
- package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +482 -0
- package/src/components/right-panel/claude/ClaudeProgressIndicator.tsx +76 -0
- package/src/components/right-panel/claude/DiffCard.tsx +130 -0
- package/src/components/right-panel/claude/DiffViewer.tsx +54 -0
- package/src/components/right-panel/claude/ProjectRootSelector.tsx +275 -0
- package/src/components/right-panel/claude/ResultsSummary.tsx +119 -0
- package/src/components/right-panel/claude/SetupFlow.tsx +315 -0
- package/src/components/right-panel/console/ConsolePanel.tsx +209 -0
- package/src/components/right-panel/design/AppearanceSection.tsx +703 -0
- package/src/components/right-panel/design/BackgroundSection.tsx +516 -0
- package/src/components/right-panel/design/BorderSection.tsx +161 -0
- package/src/components/right-panel/design/CSSRawView.tsx +412 -0
- package/src/components/right-panel/design/DesignCSSTabToggle.tsx +51 -0
- package/src/components/right-panel/design/DesignPanel.tsx +275 -0
- package/src/components/right-panel/design/ElementBreadcrumb.tsx +51 -0
- package/src/components/right-panel/design/GradientEditor.tsx +726 -0
- package/src/components/right-panel/design/LayoutSection.tsx +1948 -0
- package/src/components/right-panel/design/PositionSection.tsx +865 -0
- package/src/components/right-panel/design/PropertiesSection.tsx +86 -0
- package/src/components/right-panel/design/SVGSection.tsx +361 -0
- package/src/components/right-panel/design/ShadowBlurSection.tsx +227 -0
- package/src/components/right-panel/design/SizeSection.tsx +183 -0
- package/src/components/right-panel/design/TextSection.tsx +719 -0
- package/src/components/right-panel/design/icons.tsx +948 -0
- package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +467 -0
- package/src/components/right-panel/design/inputs/ColorInput.tsx +43 -0
- package/src/components/right-panel/design/inputs/CompactInput.tsx +333 -0
- package/src/components/right-panel/design/inputs/DraggableLabel.tsx +118 -0
- package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +54 -0
- package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +174 -0
- package/src/components/right-panel/design/inputs/SectionHeader.tsx +79 -0
- package/src/components/right-panel/variables/VariablesPanel.tsx +388 -0
- package/src/hooks/useBridge.ts +95 -0
- package/src/hooks/useChangeTracker.ts +563 -0
- package/src/hooks/useClaudeAPI.ts +118 -0
- package/src/hooks/useDOMTree.ts +25 -0
- package/src/hooks/useKeyboardShortcuts.ts +76 -0
- package/src/hooks/usePostMessage.ts +589 -0
- package/src/hooks/useProjectScan.ts +204 -0
- package/src/hooks/useResizable.ts +20 -0
- package/src/hooks/useSelectedElement.ts +51 -0
- package/src/hooks/useTargetUrl.ts +81 -0
- package/src/inspector/DOMTraverser.ts +71 -0
- package/src/inspector/ElementSelector.ts +23 -0
- package/src/inspector/HoverHighlighter.ts +54 -0
- package/src/inspector/SelectionHighlighter.ts +27 -0
- package/src/inspector/StyleExtractor.ts +19 -0
- package/src/inspector/inspector.ts +17 -0
- package/src/inspector/messaging.ts +30 -0
- package/src/lib/apiBase.ts +15 -0
- package/src/lib/classifyElement.ts +430 -0
- package/src/lib/claude-bin.ts +197 -0
- package/src/lib/claude-stream.ts +158 -0
- package/src/lib/clientProjectScanner.ts +344 -0
- package/src/lib/componentMatcher.ts +156 -0
- package/src/lib/constants.ts +573 -0
- package/src/lib/cssVariableUtils.ts +409 -0
- package/src/lib/diffParser.ts +206 -0
- package/src/lib/folderPicker.ts +84 -0
- package/src/lib/gradientParser.ts +160 -0
- package/src/lib/projectScanner.ts +355 -0
- package/src/lib/promptBuilder.ts +402 -0
- package/src/lib/shadowParser.ts +124 -0
- package/src/lib/tailwindClassParser.ts +248 -0
- package/src/lib/textShadowUtils.ts +106 -0
- package/src/lib/utils.ts +299 -0
- package/src/lib/validatePath.ts +40 -0
- package/src/proxy.ts +92 -0
- package/src/server/terminal-server.ts +104 -0
- package/src/store/changeSlice.ts +288 -0
- package/src/store/claudeSlice.ts +222 -0
- package/src/store/componentSlice.ts +90 -0
- package/src/store/consoleSlice.ts +51 -0
- package/src/store/cssVariableSlice.ts +94 -0
- package/src/store/elementSlice.ts +78 -0
- package/src/store/index.ts +35 -0
- package/src/store/terminalSlice.ts +30 -0
- package/src/store/treeSlice.ts +69 -0
- package/src/store/uiSlice.ts +327 -0
- package/src/types/changelog.ts +49 -0
- package/src/types/claude.ts +131 -0
- package/src/types/component.ts +49 -0
- package/src/types/cssVariables.ts +18 -0
- package/src/types/element.ts +21 -0
- package/src/types/file-system-access.d.ts +27 -0
- package/src/types/gradient.ts +12 -0
- package/src/types/messages.ts +392 -0
- package/src/types/shadow.ts +8 -0
- package/src/types/tree.ts +9 -0
- package/tsconfig.json +42 -0
- package/tsconfig.server.json +12 -0
|
@@ -0,0 +1,2400 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { PROXY_HEADER } from '@/lib/constants'
|
|
3
|
+
|
|
4
|
+
const INSPECTOR_SCRIPT = `
|
|
5
|
+
<script>
|
|
6
|
+
(function() {
|
|
7
|
+
${getInspectorCode()}
|
|
8
|
+
})();
|
|
9
|
+
</script>
|
|
10
|
+
`
|
|
11
|
+
|
|
12
|
+
function getInspectorCode(): string {
|
|
13
|
+
// The inspector code will be inlined here during build.
|
|
14
|
+
// For now, load it dynamically via a separate endpoint.
|
|
15
|
+
return `
|
|
16
|
+
// Inspector bootstrap - sends INSPECTOR_READY and sets up message handling
|
|
17
|
+
var DEV_EDITOR_INSPECTOR = (function() {
|
|
18
|
+
var parentOrigin = '*';
|
|
19
|
+
try { parentOrigin = window.parent.location.origin; } catch(e) { parentOrigin = '*'; }
|
|
20
|
+
|
|
21
|
+
function send(message) {
|
|
22
|
+
try { window.parent.postMessage(message, parentOrigin); } catch(e) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generateSelectorPath(element) {
|
|
26
|
+
var parts = [];
|
|
27
|
+
var current = element;
|
|
28
|
+
while (current && current !== document.documentElement) {
|
|
29
|
+
var selector = current.tagName.toLowerCase();
|
|
30
|
+
if (current.id) {
|
|
31
|
+
selector += '#' + CSS.escape(current.id);
|
|
32
|
+
parts.unshift(selector);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (current.className && typeof current.className === 'string') {
|
|
36
|
+
var classes = current.className.trim().split(/\\s+/).filter(Boolean);
|
|
37
|
+
if (classes.length > 0) {
|
|
38
|
+
selector += '.' + classes.map(function(c) { return CSS.escape(c); }).join('.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
var parent = current.parentElement;
|
|
42
|
+
if (parent) {
|
|
43
|
+
var siblings = Array.from(parent.children).filter(function(c) {
|
|
44
|
+
return c.tagName === current.tagName;
|
|
45
|
+
});
|
|
46
|
+
if (siblings.length > 1) {
|
|
47
|
+
var index = siblings.indexOf(current) + 1;
|
|
48
|
+
selector += ':nth-of-type(' + index + ')';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
parts.unshift(selector);
|
|
52
|
+
current = current.parentElement;
|
|
53
|
+
}
|
|
54
|
+
return parts.join(' > ');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function serializeTree(element) {
|
|
58
|
+
if (!element || element.nodeType !== 1) return null;
|
|
59
|
+
var tagName = element.tagName.toLowerCase();
|
|
60
|
+
if (tagName === 'script' || tagName === 'style' || tagName === 'link') return null;
|
|
61
|
+
var children = [];
|
|
62
|
+
for (var i = 0; i < element.children.length; i++) {
|
|
63
|
+
var child = serializeTree(element.children[i]);
|
|
64
|
+
if (child) children.push(child);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
id: generateSelectorPath(element),
|
|
68
|
+
tagName: tagName,
|
|
69
|
+
className: element.className && typeof element.className === 'string' ? element.className : null,
|
|
70
|
+
elementId: element.id || null,
|
|
71
|
+
children: children
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toCC(s) {
|
|
76
|
+
if (s.charAt(0) === '-') s = s.substring(1);
|
|
77
|
+
return s.replace(/-([a-z])/g, function(_, c) { return c.toUpperCase(); });
|
|
78
|
+
}
|
|
79
|
+
function toKC(s) {
|
|
80
|
+
var k = s.replace(/[A-Z]/g, function(c) { return '-' + c.toLowerCase(); });
|
|
81
|
+
if (/^(webkit|moz|ms)-/.test(k)) k = '-' + k;
|
|
82
|
+
return k;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getComputedStylesForElement(el) {
|
|
86
|
+
var computed = window.getComputedStyle(el);
|
|
87
|
+
var props = [
|
|
88
|
+
'width','height','min-width','min-height','max-width','max-height',
|
|
89
|
+
'overflow','overflow-x','overflow-y','box-sizing',
|
|
90
|
+
'margin-top','margin-right','margin-bottom','margin-left',
|
|
91
|
+
'padding-top','padding-right','padding-bottom','padding-left',
|
|
92
|
+
'font-family','font-size','font-weight','font-style','line-height','letter-spacing',
|
|
93
|
+
'text-align','text-decoration','text-transform','text-indent','text-overflow','text-shadow','color',
|
|
94
|
+
'direction','word-break','line-break','white-space','column-count',
|
|
95
|
+
'-webkit-text-stroke-width','-webkit-text-stroke-color',
|
|
96
|
+
'border-width','border-style','border-color','border-radius',
|
|
97
|
+
'border-top-width','border-right-width','border-bottom-width','border-left-width',
|
|
98
|
+
'border-top-left-radius','border-top-right-radius',
|
|
99
|
+
'border-bottom-right-radius','border-bottom-left-radius',
|
|
100
|
+
'background-color','background-image',
|
|
101
|
+
'background-size','background-position','background-repeat','background-attachment','background-clip',
|
|
102
|
+
'opacity','visibility','cursor','mix-blend-mode','pointer-events',
|
|
103
|
+
'display','flex-direction','justify-content','align-items',
|
|
104
|
+
'flex-wrap','gap','column-gap','row-gap',
|
|
105
|
+
'grid-template-columns','grid-template-rows','grid-auto-flow','justify-items',
|
|
106
|
+
'vertical-align',
|
|
107
|
+
'position','top','right','bottom','left','z-index','float','clear',
|
|
108
|
+
'box-shadow','filter'
|
|
109
|
+
];
|
|
110
|
+
var styles = {};
|
|
111
|
+
for (var i = 0; i < props.length; i++) {
|
|
112
|
+
styles[toCC(props[i])] = computed.getPropertyValue(props[i]);
|
|
113
|
+
}
|
|
114
|
+
return styles;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scanCSSVariableDefinitions() {
|
|
118
|
+
var definitions = {};
|
|
119
|
+
var scopesSet = {};
|
|
120
|
+
var rootStyles = window.getComputedStyle(document.documentElement);
|
|
121
|
+
var FRAMEWORK_PREFIXES = ['--tw-', '--next-', '--radix-', '--chakra-', '--mantine-', '--mui-', '--framer-', '--sb-'];
|
|
122
|
+
|
|
123
|
+
// Detect Tailwind v4 @theme usage — if present, keep --color-*, --font-*, --spacing-* vars
|
|
124
|
+
var hasTailwindTheme = false;
|
|
125
|
+
for (var tsi = 0; tsi < document.styleSheets.length; tsi++) {
|
|
126
|
+
var tSheet = document.styleSheets[tsi];
|
|
127
|
+
try {
|
|
128
|
+
// Check inline <style> content for @theme directive
|
|
129
|
+
if (tSheet.ownerNode && tSheet.ownerNode.textContent && tSheet.ownerNode.textContent.indexOf('@theme') >= 0) {
|
|
130
|
+
hasTailwindTheme = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
// Check linked stylesheet rules for @layer theme or @theme
|
|
134
|
+
var tRules = tSheet.cssRules || tSheet.rules;
|
|
135
|
+
if (tRules) {
|
|
136
|
+
for (var tri = 0; tri < tRules.length; tri++) {
|
|
137
|
+
var tRule = tRules[tri];
|
|
138
|
+
if (tRule.cssText && tRule.cssText.indexOf('@theme') >= 0) {
|
|
139
|
+
hasTailwindTheme = true;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch(e) { /* cross-origin stylesheet */ }
|
|
145
|
+
if (hasTailwindTheme) break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function classifyScope(selectorText, parentRule) {
|
|
149
|
+
if (!selectorText) return 'custom';
|
|
150
|
+
var sel = selectorText.trim().toLowerCase();
|
|
151
|
+
// Check if inside a @media (prefers-color-scheme: dark) block
|
|
152
|
+
if (parentRule && parentRule.conditionText) {
|
|
153
|
+
var cond = parentRule.conditionText.toLowerCase();
|
|
154
|
+
if (cond.indexOf('prefers-color-scheme') >= 0 && cond.indexOf('dark') >= 0) {
|
|
155
|
+
return 'media-dark';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (sel === ':root' || sel === ':root, :host' || sel === ':host, :root') return 'root';
|
|
159
|
+
if (sel.indexOf('.dark') >= 0 || sel.indexOf('[data-theme="dark"]') >= 0 || sel.indexOf('[data-mode="dark"]') >= 0) return 'dark';
|
|
160
|
+
return 'root';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function extractFromRules(rules, parentRule) {
|
|
164
|
+
for (var ri = 0; ri < rules.length; ri++) {
|
|
165
|
+
var rule = rules[ri];
|
|
166
|
+
if (rule.cssRules) {
|
|
167
|
+
extractFromRules(rule.cssRules, rule);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!rule.style) continue;
|
|
171
|
+
var ruleSelector = rule.selectorText || '';
|
|
172
|
+
var scope = classifyScope(ruleSelector, parentRule);
|
|
173
|
+
if (ruleSelector) scopesSet[ruleSelector] = true;
|
|
174
|
+
for (var pi = 0; pi < rule.style.length; pi++) {
|
|
175
|
+
var prop = rule.style[pi];
|
|
176
|
+
if (prop.indexOf('--') === 0) {
|
|
177
|
+
var rawVal = rule.style.getPropertyValue(prop).trim();
|
|
178
|
+
var resolved = rootStyles.getPropertyValue(prop).trim();
|
|
179
|
+
definitions[prop] = {
|
|
180
|
+
value: rawVal,
|
|
181
|
+
resolvedValue: resolved || rawVal,
|
|
182
|
+
selector: ruleSelector,
|
|
183
|
+
scope: scope
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
var taggedSheets = [];
|
|
191
|
+
for (var si = 0; si < document.styleSheets.length; si++) {
|
|
192
|
+
var sheet = document.styleSheets[si];
|
|
193
|
+
if (sheet.ownerNode && sheet.ownerNode.hasAttribute && sheet.ownerNode.hasAttribute('data-design-tokens')) {
|
|
194
|
+
taggedSheets.push(sheet);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (taggedSheets.length > 0) {
|
|
199
|
+
for (var ti = 0; ti < taggedSheets.length; ti++) {
|
|
200
|
+
var taggedRules;
|
|
201
|
+
try { taggedRules = taggedSheets[ti].cssRules || taggedSheets[ti].rules; } catch(e) { continue; }
|
|
202
|
+
if (taggedRules) extractFromRules(taggedRules, null);
|
|
203
|
+
}
|
|
204
|
+
return { definitions: definitions, isExplicit: true, scopes: Object.keys(scopesSet) };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (var fi = 0; fi < document.styleSheets.length; fi++) {
|
|
208
|
+
var fallbackSheet = document.styleSheets[fi];
|
|
209
|
+
var fallbackRules;
|
|
210
|
+
try { fallbackRules = fallbackSheet.cssRules || fallbackSheet.rules; } catch(e) { continue; }
|
|
211
|
+
if (fallbackRules) extractFromRules(fallbackRules, null);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
var metaEl = document.querySelector('meta[name="design-tokens-prefix"]');
|
|
215
|
+
var metaPrefixes = null;
|
|
216
|
+
if (metaEl) {
|
|
217
|
+
var content = metaEl.getAttribute('content');
|
|
218
|
+
if (content) {
|
|
219
|
+
metaPrefixes = content.split(',');
|
|
220
|
+
for (var mpi = 0; mpi < metaPrefixes.length; mpi++) {
|
|
221
|
+
metaPrefixes[mpi] = metaPrefixes[mpi].trim();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
var filtered = {};
|
|
227
|
+
var keys = Object.keys(definitions);
|
|
228
|
+
for (var ki = 0; ki < keys.length; ki++) {
|
|
229
|
+
var key = keys[ki];
|
|
230
|
+
if (metaPrefixes) {
|
|
231
|
+
var allowed = false;
|
|
232
|
+
for (var api = 0; api < metaPrefixes.length; api++) {
|
|
233
|
+
if (key.indexOf(metaPrefixes[api]) === 0) { allowed = true; break; }
|
|
234
|
+
}
|
|
235
|
+
if (allowed) filtered[key] = definitions[key];
|
|
236
|
+
} else {
|
|
237
|
+
var isFramework = false;
|
|
238
|
+
for (var fpi = 0; fpi < FRAMEWORK_PREFIXES.length; fpi++) {
|
|
239
|
+
if (key.indexOf(FRAMEWORK_PREFIXES[fpi]) === 0) { isFramework = true; break; }
|
|
240
|
+
}
|
|
241
|
+
// If Tailwind v4 @theme detected, keep --color-*, --font-*, --spacing-* even if --tw- internal vars are filtered
|
|
242
|
+
if (isFramework && hasTailwindTheme && key.indexOf('--tw-') !== 0) {
|
|
243
|
+
isFramework = false;
|
|
244
|
+
}
|
|
245
|
+
if (!isFramework) filtered[key] = definitions[key];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { definitions: filtered, isExplicit: false, scopes: Object.keys(scopesSet) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function detectCSSVariablesOnElement(el) {
|
|
253
|
+
var usages = {};
|
|
254
|
+
// Check inline style for var() references
|
|
255
|
+
if (el.style) {
|
|
256
|
+
for (var si = 0; si < el.style.length; si++) {
|
|
257
|
+
var inlineProp = el.style[si];
|
|
258
|
+
var inlineVal = el.style.getPropertyValue(inlineProp);
|
|
259
|
+
if (inlineVal && inlineVal.indexOf('var(') >= 0) {
|
|
260
|
+
usages[inlineProp] = inlineVal.trim();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Check stylesheet rules that match this element
|
|
265
|
+
for (var shi = 0; shi < document.styleSheets.length; shi++) {
|
|
266
|
+
var sheet = document.styleSheets[shi];
|
|
267
|
+
var rules;
|
|
268
|
+
try { rules = sheet.cssRules || sheet.rules; } catch(e) { continue; }
|
|
269
|
+
if (!rules) continue;
|
|
270
|
+
for (var ri = 0; ri < rules.length; ri++) {
|
|
271
|
+
var rule = rules[ri];
|
|
272
|
+
if (!rule.selectorText || !rule.style) continue;
|
|
273
|
+
var matches = false;
|
|
274
|
+
try { matches = el.matches(rule.selectorText); } catch(e) { continue; }
|
|
275
|
+
if (!matches) continue;
|
|
276
|
+
for (var pi = 0; pi < rule.style.length; pi++) {
|
|
277
|
+
var prop = rule.style[pi];
|
|
278
|
+
var val = rule.style.getPropertyValue(prop);
|
|
279
|
+
if (val && val.indexOf('var(') >= 0) {
|
|
280
|
+
// Don't overwrite inline style usages (higher specificity)
|
|
281
|
+
if (!usages[prop]) {
|
|
282
|
+
usages[prop] = val.trim();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return usages;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Selection highlight
|
|
292
|
+
var selectionOverlay = document.createElement('div');
|
|
293
|
+
selectionOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:999997;border:2px solid #4a9eff;display:none;';
|
|
294
|
+
document.body.appendChild(selectionOverlay);
|
|
295
|
+
|
|
296
|
+
// Hover highlight — dotted green border + element name label (visual-editor-style)
|
|
297
|
+
var hoverOverlay = document.createElement('div');
|
|
298
|
+
hoverOverlay.style.cssText = 'position:fixed;pointer-events:none;z-index:999996;border:1px dashed #4ade80;display:none;transition:top 0.04s,left 0.04s,width 0.04s,height 0.04s;';
|
|
299
|
+
document.body.appendChild(hoverOverlay);
|
|
300
|
+
|
|
301
|
+
var hoverLabel = document.createElement('div');
|
|
302
|
+
hoverLabel.style.cssText = 'position:absolute;top:-18px;left:-1px;padding:1px 6px;font-size:10px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;line-height:14px;color:#fff;background:#1D3F23;border-radius:3px 3px 0 0;white-space:nowrap;pointer-events:none;';
|
|
303
|
+
hoverOverlay.appendChild(hoverLabel);
|
|
304
|
+
|
|
305
|
+
var hoveredElement = null;
|
|
306
|
+
|
|
307
|
+
function getElementLabel(el) {
|
|
308
|
+
var tag = el.tagName.toLowerCase();
|
|
309
|
+
// Show id if present
|
|
310
|
+
if (el.id) return tag + '#' + el.id;
|
|
311
|
+
// Show first meaningful class
|
|
312
|
+
var cls = el.className;
|
|
313
|
+
if (cls && typeof cls === 'string') {
|
|
314
|
+
var first = cls.trim().split(/\s+/)[0];
|
|
315
|
+
if (first) return tag + '.' + first;
|
|
316
|
+
}
|
|
317
|
+
return tag;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
document.addEventListener('mousemove', function(e) {
|
|
321
|
+
if (!selectionModeEnabled) { hoverOverlay.style.display = 'none'; return; }
|
|
322
|
+
var el = document.elementFromPoint(e.clientX, e.clientY);
|
|
323
|
+
if (!el || el === hoverOverlay || el === selectionOverlay || el === hoverLabel) return;
|
|
324
|
+
// Don't show hover on already-selected element
|
|
325
|
+
if (el === selectedElement) { hoverOverlay.style.display = 'none'; hoveredElement = null; return; }
|
|
326
|
+
if (el === hoveredElement) return;
|
|
327
|
+
hoveredElement = el;
|
|
328
|
+
var rect = el.getBoundingClientRect();
|
|
329
|
+
hoverOverlay.style.display = 'block';
|
|
330
|
+
hoverOverlay.style.top = rect.top + 'px';
|
|
331
|
+
hoverOverlay.style.left = rect.left + 'px';
|
|
332
|
+
hoverOverlay.style.width = rect.width + 'px';
|
|
333
|
+
hoverOverlay.style.height = rect.height + 'px';
|
|
334
|
+
hoverLabel.textContent = getElementLabel(el);
|
|
335
|
+
// If element is near top of viewport, show label below instead
|
|
336
|
+
if (rect.top < 20) {
|
|
337
|
+
hoverLabel.style.top = 'auto';
|
|
338
|
+
hoverLabel.style.bottom = '-18px';
|
|
339
|
+
hoverLabel.style.borderRadius = '0 0 3px 3px';
|
|
340
|
+
} else {
|
|
341
|
+
hoverLabel.style.top = '-18px';
|
|
342
|
+
hoverLabel.style.bottom = 'auto';
|
|
343
|
+
hoverLabel.style.borderRadius = '3px 3px 0 0';
|
|
344
|
+
}
|
|
345
|
+
}, true);
|
|
346
|
+
|
|
347
|
+
document.addEventListener('mouseleave', function() {
|
|
348
|
+
hoverOverlay.style.display = 'none';
|
|
349
|
+
hoveredElement = null;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
function getReactSourceInfo(element) {
|
|
353
|
+
// Find React fiber key on DOM element
|
|
354
|
+
var fiberKey = null;
|
|
355
|
+
var keys = Object.keys(element);
|
|
356
|
+
for (var i = 0; i < keys.length; i++) {
|
|
357
|
+
if (keys[i].indexOf('__reactFiber$') === 0 || keys[i].indexOf('__reactInternalInstance$') === 0) {
|
|
358
|
+
fiberKey = keys[i]; break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!fiberKey) return null;
|
|
362
|
+
|
|
363
|
+
var fiber = element[fiberKey];
|
|
364
|
+
var source = null;
|
|
365
|
+
var componentName = null;
|
|
366
|
+
var componentChain = [];
|
|
367
|
+
var visited = 0;
|
|
368
|
+
|
|
369
|
+
while (fiber && visited < 50) {
|
|
370
|
+
visited++;
|
|
371
|
+
if (!source && fiber._debugSource) {
|
|
372
|
+
source = {
|
|
373
|
+
fileName: fiber._debugSource.fileName || '',
|
|
374
|
+
lineNumber: fiber._debugSource.lineNumber || 0,
|
|
375
|
+
columnNumber: fiber._debugSource.columnNumber
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
// Collect component names (function/class components, forwardRef)
|
|
379
|
+
var type = fiber.type;
|
|
380
|
+
if (type) {
|
|
381
|
+
var name = null;
|
|
382
|
+
if (typeof type === 'function') {
|
|
383
|
+
name = type.displayName || type.name;
|
|
384
|
+
} else if (type.$$typeof && (type.render || type.type)) {
|
|
385
|
+
var inner = type.render || type.type;
|
|
386
|
+
if (typeof inner === 'function') name = inner.displayName || inner.name;
|
|
387
|
+
}
|
|
388
|
+
if (name && name !== 'Anonymous' && name.length > 1) {
|
|
389
|
+
if (!componentName) componentName = name;
|
|
390
|
+
componentChain.push(name);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (source && componentChain.length >= 10) break;
|
|
394
|
+
fiber = fiber.return;
|
|
395
|
+
}
|
|
396
|
+
if (!source) return null;
|
|
397
|
+
return {
|
|
398
|
+
fileName: source.fileName,
|
|
399
|
+
lineNumber: source.lineNumber,
|
|
400
|
+
columnNumber: source.columnNumber,
|
|
401
|
+
componentName: componentName,
|
|
402
|
+
componentChain: componentChain
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
var selectedElement = null;
|
|
407
|
+
var selectionModeEnabled = true;
|
|
408
|
+
|
|
409
|
+
// Click selection — when selection mode is on, intercept clicks to select elements.
|
|
410
|
+
// When off, let clicks through so links and buttons work normally.
|
|
411
|
+
document.addEventListener('click', function(e) {
|
|
412
|
+
if (!selectionModeEnabled) return;
|
|
413
|
+
|
|
414
|
+
// If text editing is active, clicking outside commits the edit
|
|
415
|
+
if (textEditingActive) {
|
|
416
|
+
var clickedEl = document.elementFromPoint(e.clientX, e.clientY);
|
|
417
|
+
if (clickedEl !== textEditTarget) {
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
e.stopPropagation();
|
|
420
|
+
commitTextEdit();
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
e.preventDefault();
|
|
426
|
+
e.stopPropagation();
|
|
427
|
+
var el = document.elementFromPoint(e.clientX, e.clientY);
|
|
428
|
+
if (!el || el === selectionOverlay || el === hoverOverlay || el === hoverLabel) return;
|
|
429
|
+
hoverOverlay.style.display = 'none';
|
|
430
|
+
selectElement(el);
|
|
431
|
+
}, true);
|
|
432
|
+
|
|
433
|
+
function selectElement(el) {
|
|
434
|
+
// Don't select elements when selection mode is disabled (preview mode)
|
|
435
|
+
if (!selectionModeEnabled) return;
|
|
436
|
+
selectedElement = el;
|
|
437
|
+
// Hide hover overlay when selecting
|
|
438
|
+
hoverOverlay.style.display = 'none';
|
|
439
|
+
hoveredElement = null;
|
|
440
|
+
var rect = el.getBoundingClientRect();
|
|
441
|
+
selectionOverlay.style.display = 'block';
|
|
442
|
+
selectionOverlay.style.top = rect.top + 'px';
|
|
443
|
+
selectionOverlay.style.left = rect.left + 'px';
|
|
444
|
+
selectionOverlay.style.width = rect.width + 'px';
|
|
445
|
+
selectionOverlay.style.height = rect.height + 'px';
|
|
446
|
+
|
|
447
|
+
// Collect all attributes
|
|
448
|
+
var attrs = {};
|
|
449
|
+
for (var ai = 0; ai < el.attributes.length; ai++) {
|
|
450
|
+
var attr = el.attributes[ai];
|
|
451
|
+
attrs[attr.name] = attr.value;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Collect truncated inner text
|
|
455
|
+
var text = (el.innerText || '').substring(0, 500) || null;
|
|
456
|
+
|
|
457
|
+
var varUsages = detectCSSVariablesOnElement(el);
|
|
458
|
+
console.log('[pAInt] CSS variable usages for', el.tagName, el.className, varUsages);
|
|
459
|
+
|
|
460
|
+
var sourceInfo = getReactSourceInfo(el);
|
|
461
|
+
if (sourceInfo) {
|
|
462
|
+
console.log('[pAInt] sourceInfo:', sourceInfo.fileName + ':' + sourceInfo.lineNumber, 'component:', sourceInfo.componentName, 'chain:', sourceInfo.componentChain.join(' > '));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
send({
|
|
466
|
+
type: 'ELEMENT_SELECTED',
|
|
467
|
+
payload: {
|
|
468
|
+
selectorPath: generateSelectorPath(el),
|
|
469
|
+
tagName: el.tagName.toLowerCase(),
|
|
470
|
+
className: el.className && typeof el.className === 'string' ? el.className : null,
|
|
471
|
+
id: el.id || null,
|
|
472
|
+
attributes: attrs,
|
|
473
|
+
innerText: text,
|
|
474
|
+
computedStyles: getComputedStylesForElement(el),
|
|
475
|
+
cssVariableUsages: varUsages,
|
|
476
|
+
sourceInfo: sourceInfo,
|
|
477
|
+
boundingRect: {
|
|
478
|
+
x: rect.x, y: rect.y,
|
|
479
|
+
width: rect.width, height: rect.height,
|
|
480
|
+
top: rect.top, right: rect.right,
|
|
481
|
+
bottom: rect.bottom, left: rect.left
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function clearSelection() {
|
|
488
|
+
selectedElement = null;
|
|
489
|
+
selectionOverlay.style.display = 'none';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Update overlays on scroll so they follow elements
|
|
493
|
+
function updateOverlays() {
|
|
494
|
+
if (selectedElement && selectionOverlay.style.display !== 'none') {
|
|
495
|
+
var sr = selectedElement.getBoundingClientRect();
|
|
496
|
+
selectionOverlay.style.top = sr.top + 'px';
|
|
497
|
+
selectionOverlay.style.left = sr.left + 'px';
|
|
498
|
+
selectionOverlay.style.width = sr.width + 'px';
|
|
499
|
+
selectionOverlay.style.height = sr.height + 'px';
|
|
500
|
+
}
|
|
501
|
+
if (hoveredElement && hoverOverlay.style.display !== 'none') {
|
|
502
|
+
var hr = hoveredElement.getBoundingClientRect();
|
|
503
|
+
hoverOverlay.style.top = hr.top + 'px';
|
|
504
|
+
hoverOverlay.style.left = hr.left + 'px';
|
|
505
|
+
hoverOverlay.style.width = hr.width + 'px';
|
|
506
|
+
hoverOverlay.style.height = hr.height + 'px';
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
window.addEventListener('scroll', updateOverlays, true);
|
|
510
|
+
window.addEventListener('resize', updateOverlays, true);
|
|
511
|
+
|
|
512
|
+
// --- Inline Text Editing ---
|
|
513
|
+
var textEditingActive = false;
|
|
514
|
+
var originalTextContent = null;
|
|
515
|
+
var textEditTarget = null;
|
|
516
|
+
var SKIP_TEXT_EDIT_TAGS = { INPUT: 1, TEXTAREA: 1, SELECT: 1, IMG: 1, VIDEO: 1, IFRAME: 1, SVG: 1, svg: 1, CANVAS: 1 };
|
|
517
|
+
|
|
518
|
+
function commitTextEdit() {
|
|
519
|
+
if (!textEditingActive || !textEditTarget) return;
|
|
520
|
+
var newText = textEditTarget.textContent || '';
|
|
521
|
+
var el = textEditTarget;
|
|
522
|
+
var selectorPath = generateSelectorPath(el);
|
|
523
|
+
|
|
524
|
+
el.contentEditable = 'false';
|
|
525
|
+
el.style.removeProperty('outline');
|
|
526
|
+
el.style.removeProperty('outline-offset');
|
|
527
|
+
el.style.removeProperty('min-width');
|
|
528
|
+
textEditingActive = false;
|
|
529
|
+
textEditTarget = null;
|
|
530
|
+
|
|
531
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'id', 'style'] });
|
|
532
|
+
|
|
533
|
+
selectionOverlay.style.display = 'block';
|
|
534
|
+
updateOverlays();
|
|
535
|
+
|
|
536
|
+
if (newText !== originalTextContent) {
|
|
537
|
+
send({
|
|
538
|
+
type: 'TEXT_CHANGED',
|
|
539
|
+
payload: {
|
|
540
|
+
selectorPath: selectorPath,
|
|
541
|
+
originalText: originalTextContent || '',
|
|
542
|
+
newText: newText
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
originalTextContent = null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function cancelTextEdit() {
|
|
550
|
+
if (!textEditingActive || !textEditTarget) return;
|
|
551
|
+
textEditTarget.textContent = originalTextContent;
|
|
552
|
+
textEditTarget.contentEditable = 'false';
|
|
553
|
+
textEditTarget.style.removeProperty('outline');
|
|
554
|
+
textEditTarget.style.removeProperty('outline-offset');
|
|
555
|
+
textEditTarget.style.removeProperty('min-width');
|
|
556
|
+
textEditingActive = false;
|
|
557
|
+
textEditTarget = null;
|
|
558
|
+
originalTextContent = null;
|
|
559
|
+
|
|
560
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'id', 'style'] });
|
|
561
|
+
|
|
562
|
+
selectionOverlay.style.display = 'block';
|
|
563
|
+
updateOverlays();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Find the best editable text target from a starting element.
|
|
567
|
+
// Walks down through wrappers to find a leaf text node,
|
|
568
|
+
// or returns the element itself if it has direct text content.
|
|
569
|
+
// Handles: <button><svg/>Submit</button>, <a><svg/><span>text</span><svg/></a>, etc.
|
|
570
|
+
function findTextTarget(el) {
|
|
571
|
+
if (!el || SKIP_TEXT_EDIT_TAGS[el.tagName]) return null;
|
|
572
|
+
// Leaf node with text — ideal target
|
|
573
|
+
if (el.children.length === 0) {
|
|
574
|
+
return (el.textContent && el.textContent.trim()) ? el : null;
|
|
575
|
+
}
|
|
576
|
+
// Single child — recurse into it (common: <button><span>text</span></button>)
|
|
577
|
+
if (el.children.length === 1) {
|
|
578
|
+
var child = el.children[0];
|
|
579
|
+
if (SKIP_TEXT_EDIT_TAGS[child.tagName]) {
|
|
580
|
+
return hasDirectTextNodes(el) ? el : null;
|
|
581
|
+
}
|
|
582
|
+
return findTextTarget(child);
|
|
583
|
+
}
|
|
584
|
+
// Multiple children — find the single non-skippable text-bearing child
|
|
585
|
+
// (handles: <a><svg/><span>Book a Call</span><svg/></a>)
|
|
586
|
+
var textChild = null;
|
|
587
|
+
for (var i = 0; i < el.children.length; i++) {
|
|
588
|
+
var ch = el.children[i];
|
|
589
|
+
if (SKIP_TEXT_EDIT_TAGS[ch.tagName]) continue;
|
|
590
|
+
if (ch.textContent && ch.textContent.trim()) {
|
|
591
|
+
if (textChild) { textChild = null; break; } // ambiguous — multiple text children
|
|
592
|
+
textChild = ch;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (textChild) return findTextTarget(textChild);
|
|
596
|
+
// Fall back: allow editing if element has direct text nodes
|
|
597
|
+
return hasDirectTextNodes(el) ? el : null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function hasDirectTextNodes(el) {
|
|
601
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
602
|
+
if (el.childNodes[i].nodeType === 3 && el.childNodes[i].textContent.trim()) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
document.addEventListener('dblclick', function(e) {
|
|
610
|
+
if (!selectionModeEnabled) return;
|
|
611
|
+
e.preventDefault();
|
|
612
|
+
e.stopPropagation();
|
|
613
|
+
|
|
614
|
+
var el = document.elementFromPoint(e.clientX, e.clientY);
|
|
615
|
+
// If the selection overlay is in the way, temporarily hide it
|
|
616
|
+
// and re-probe to get the actual element underneath
|
|
617
|
+
if (el === selectionOverlay || el === hoverOverlay) {
|
|
618
|
+
var prevSel = selectionOverlay.style.display;
|
|
619
|
+
var prevHov = hoverOverlay.style.display;
|
|
620
|
+
selectionOverlay.style.display = 'none';
|
|
621
|
+
hoverOverlay.style.display = 'none';
|
|
622
|
+
el = document.elementFromPoint(e.clientX, e.clientY);
|
|
623
|
+
selectionOverlay.style.display = prevSel;
|
|
624
|
+
hoverOverlay.style.display = prevHov;
|
|
625
|
+
}
|
|
626
|
+
if (!el) return;
|
|
627
|
+
if (SKIP_TEXT_EDIT_TAGS[el.tagName]) return;
|
|
628
|
+
|
|
629
|
+
// Find the best editable text target (may walk down into children)
|
|
630
|
+
el = findTextTarget(el);
|
|
631
|
+
if (!el) return;
|
|
632
|
+
|
|
633
|
+
var text = el.textContent;
|
|
634
|
+
if (text === null || text === undefined) return;
|
|
635
|
+
|
|
636
|
+
textEditingActive = true;
|
|
637
|
+
textEditTarget = el;
|
|
638
|
+
originalTextContent = text;
|
|
639
|
+
|
|
640
|
+
observer.disconnect();
|
|
641
|
+
|
|
642
|
+
selectionOverlay.style.display = 'none';
|
|
643
|
+
|
|
644
|
+
el.contentEditable = 'true';
|
|
645
|
+
el.style.setProperty('outline', '2px solid #4a9eff', 'important');
|
|
646
|
+
el.style.setProperty('outline-offset', '2px', 'important');
|
|
647
|
+
el.style.setProperty('min-width', '20px', 'important');
|
|
648
|
+
el.focus();
|
|
649
|
+
|
|
650
|
+
var range = document.createRange();
|
|
651
|
+
range.selectNodeContents(el);
|
|
652
|
+
var sel = window.getSelection();
|
|
653
|
+
sel.removeAllRanges();
|
|
654
|
+
sel.addRange(range);
|
|
655
|
+
}, true);
|
|
656
|
+
|
|
657
|
+
document.addEventListener('keydown', function(e) {
|
|
658
|
+
if (textEditingActive) {
|
|
659
|
+
e.stopPropagation();
|
|
660
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
661
|
+
e.preventDefault();
|
|
662
|
+
commitTextEdit();
|
|
663
|
+
} else if (e.key === 'Escape') {
|
|
664
|
+
e.preventDefault();
|
|
665
|
+
cancelTextEdit();
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Delete selected element (Delete or Backspace when not editing text)
|
|
671
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && selectionModeEnabled && selectedElement) {
|
|
672
|
+
e.preventDefault();
|
|
673
|
+
e.stopPropagation();
|
|
674
|
+
var delEl = selectedElement;
|
|
675
|
+
var computed = window.getComputedStyle(delEl);
|
|
676
|
+
var origDisplay = computed.getPropertyValue('display');
|
|
677
|
+
var delSelector = generateSelectorPath(delEl);
|
|
678
|
+
|
|
679
|
+
var delAttrs = {};
|
|
680
|
+
for (var dai = 0; dai < delEl.attributes.length; dai++) {
|
|
681
|
+
var da = delEl.attributes[dai];
|
|
682
|
+
delAttrs[da.name] = da.value;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Hide the element
|
|
686
|
+
delEl.style.setProperty('display', 'none', 'important');
|
|
687
|
+
selectionOverlay.style.display = 'none';
|
|
688
|
+
selectedElement = null;
|
|
689
|
+
|
|
690
|
+
send({
|
|
691
|
+
type: 'ELEMENT_DELETED',
|
|
692
|
+
payload: {
|
|
693
|
+
selectorPath: delSelector,
|
|
694
|
+
originalDisplay: origDisplay,
|
|
695
|
+
tagName: delEl.tagName.toLowerCase(),
|
|
696
|
+
className: delEl.className && typeof delEl.className === 'string' ? delEl.className : null,
|
|
697
|
+
elementId: delEl.id || null,
|
|
698
|
+
innerText: (delEl.innerText || '').substring(0, 500) || null,
|
|
699
|
+
attributes: delAttrs,
|
|
700
|
+
computedStyles: getComputedStylesForElement(delEl)
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}, true);
|
|
705
|
+
|
|
706
|
+
// --- Component Detection ---
|
|
707
|
+
var SEMANTIC_COMPONENTS = {
|
|
708
|
+
button: 'Button', nav: 'Navigation', input: 'Input', header: 'Header',
|
|
709
|
+
footer: 'Footer', dialog: 'Dialog', a: 'Link', img: 'Image',
|
|
710
|
+
form: 'Form', select: 'Select', textarea: 'Textarea', table: 'Table',
|
|
711
|
+
aside: 'Sidebar', main: 'Main Content', section: 'Section'
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
var ARIA_ROLE_MAP = {
|
|
715
|
+
button: 'Button', navigation: 'Navigation', tab: 'Tab', tablist: 'Tab List',
|
|
716
|
+
dialog: 'Dialog', alert: 'Alert', menu: 'Menu', menuitem: 'Menu Item',
|
|
717
|
+
search: 'Search'
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
var CLASS_PATTERNS = [
|
|
721
|
+
[/\\bbtn\\b/i, 'Button'], [/\\bcard\\b/i, 'Card'], [/\\bmodal\\b/i, 'Modal'],
|
|
722
|
+
[/\\bdropdown\\b/i, 'Dropdown'], [/\\bbadge\\b/i, 'Badge'],
|
|
723
|
+
[/\\bnav\\b/i, 'Navigation'], [/\\balert\\b/i, 'Alert'], [/\\btabs?\\b/i, 'Tab']
|
|
724
|
+
];
|
|
725
|
+
|
|
726
|
+
function detectSingleComponent(el) {
|
|
727
|
+
var tag = el.tagName.toLowerCase();
|
|
728
|
+
// Priority 1: data-component attribute
|
|
729
|
+
var dataComp = el.getAttribute('data-component');
|
|
730
|
+
if (dataComp) return { name: dataComp, method: 'data-attribute' };
|
|
731
|
+
// Priority 2: custom element (tag with hyphen)
|
|
732
|
+
if (tag.indexOf('-') >= 0) return { name: tag, method: 'custom-element' };
|
|
733
|
+
// Priority 3: semantic HTML
|
|
734
|
+
if (SEMANTIC_COMPONENTS[tag]) return { name: SEMANTIC_COMPONENTS[tag], method: 'semantic-html' };
|
|
735
|
+
// Priority 4: ARIA role
|
|
736
|
+
var role = el.getAttribute('role');
|
|
737
|
+
if (role && ARIA_ROLE_MAP[role]) return { name: ARIA_ROLE_MAP[role], method: 'aria-role' };
|
|
738
|
+
// Priority 5: class pattern
|
|
739
|
+
var cls = el.className;
|
|
740
|
+
if (cls && typeof cls === 'string') {
|
|
741
|
+
for (var pi = 0; pi < CLASS_PATTERNS.length; pi++) {
|
|
742
|
+
if (CLASS_PATTERNS[pi][0].test(cls)) return { name: CLASS_PATTERNS[pi][1], method: 'class-pattern' };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function titleWords(str) {
|
|
749
|
+
return str.replace(/\\w\\S*/g, function(w) {
|
|
750
|
+
return w.charAt(0).toUpperCase() + w.substr(1).toLowerCase();
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function contextualName(el, baseName) {
|
|
755
|
+
var tag = el.tagName.toLowerCase();
|
|
756
|
+
// aria-label: most reliable semantic hint
|
|
757
|
+
var label = (el.getAttribute('aria-label') || '').trim();
|
|
758
|
+
if (label.length > 0 && label.length <= 25) return titleWords(label);
|
|
759
|
+
// title attribute
|
|
760
|
+
var titleAttr = (el.getAttribute('title') || '').trim();
|
|
761
|
+
if (titleAttr.length > 0 && titleAttr.length <= 25) return titleWords(titleAttr);
|
|
762
|
+
// Images: use alt text
|
|
763
|
+
if (tag === 'img') {
|
|
764
|
+
var alt = (el.getAttribute('alt') || '').trim();
|
|
765
|
+
if (alt.length > 0 && alt.length <= 25) return titleWords(alt);
|
|
766
|
+
}
|
|
767
|
+
// Inputs: use placeholder or type
|
|
768
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
769
|
+
var ph = (el.getAttribute('placeholder') || '').trim();
|
|
770
|
+
if (ph.length > 0 && ph.length <= 20) return titleWords(ph);
|
|
771
|
+
var tp = el.getAttribute('type');
|
|
772
|
+
if (tp && tp !== 'text' && tp !== 'hidden') return titleWords(tp) + ' ' + baseName;
|
|
773
|
+
}
|
|
774
|
+
// Buttons/links: use short inner text
|
|
775
|
+
if (tag === 'button' || tag === 'a') {
|
|
776
|
+
var txt = (el.innerText || '').trim();
|
|
777
|
+
if (txt.length > 0 && txt.length <= 20 && txt.indexOf('\\n') === -1) return titleWords(txt);
|
|
778
|
+
}
|
|
779
|
+
// id attribute: convert to readable name
|
|
780
|
+
if (el.id && el.id.length > 1 && el.id.length <= 25) {
|
|
781
|
+
return titleWords(el.id.replace(/[-_]/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2'));
|
|
782
|
+
}
|
|
783
|
+
return baseName;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
var SIZE_SUFFIXES = ['xs','sm','md','lg','xl','2xl'];
|
|
787
|
+
var COLOR_SUFFIXES = ['primary','secondary','success','danger','warning','info','light','dark'];
|
|
788
|
+
var STATE_SUFFIXES = ['active','disabled','loading','selected','checked'];
|
|
789
|
+
|
|
790
|
+
function detectClassVariants(el) {
|
|
791
|
+
var groups = [];
|
|
792
|
+
var cls = el.className;
|
|
793
|
+
if (!cls || typeof cls !== 'string') return groups;
|
|
794
|
+
var classes = cls.trim().split(/\\s+/).filter(Boolean);
|
|
795
|
+
// Extract base prefixes (e.g., "btn" from "btn-primary")
|
|
796
|
+
var prefixes = {};
|
|
797
|
+
for (var ci = 0; ci < classes.length; ci++) {
|
|
798
|
+
var parts = classes[ci].split('-');
|
|
799
|
+
if (parts.length >= 2) {
|
|
800
|
+
var base = parts[0];
|
|
801
|
+
var suffix = parts.slice(1).join('-');
|
|
802
|
+
if (!prefixes[base]) prefixes[base] = { currentClass: classes[ci], suffix: suffix };
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// Scan stylesheets for matching classes
|
|
806
|
+
for (var base in prefixes) {
|
|
807
|
+
if (!prefixes.hasOwnProperty(base)) continue;
|
|
808
|
+
var sizeOpts = [], colorOpts = [], stateOpts = [];
|
|
809
|
+
var currentClass = prefixes[base].currentClass;
|
|
810
|
+
// Scan all accessible stylesheets
|
|
811
|
+
for (var si = 0; si < document.styleSheets.length; si++) {
|
|
812
|
+
var sheet = document.styleSheets[si];
|
|
813
|
+
var rules;
|
|
814
|
+
try { rules = sheet.cssRules || sheet.rules; } catch(e) { continue; }
|
|
815
|
+
if (!rules) continue;
|
|
816
|
+
for (var ri = 0; ri < rules.length; ri++) {
|
|
817
|
+
var rule = rules[ri];
|
|
818
|
+
if (!rule.selectorText) continue;
|
|
819
|
+
var escapedBase = base.replace(/[-\\/\\\\^*+?.()|\\[\\]]/g, '\\\\' + String.fromCharCode(36) + '&');
|
|
820
|
+
var match = rule.selectorText.match(new RegExp('\\\\.' + escapedBase + '-([\\\\w-]+)'));
|
|
821
|
+
if (!match) continue;
|
|
822
|
+
var foundSuffix = match[1];
|
|
823
|
+
var foundClass = base + '-' + foundSuffix;
|
|
824
|
+
if (foundClass === currentClass) continue; // skip current
|
|
825
|
+
var opt = { label: foundSuffix, className: foundClass, removeClassNames: [currentClass], pseudoState: null, pseudoStyles: null };
|
|
826
|
+
if (SIZE_SUFFIXES.indexOf(foundSuffix) >= 0) { sizeOpts.push(opt); }
|
|
827
|
+
else if (COLOR_SUFFIXES.indexOf(foundSuffix) >= 0) { colorOpts.push(opt); }
|
|
828
|
+
else if (STATE_SUFFIXES.indexOf(foundSuffix) >= 0) { stateOpts.push(opt); }
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Build groups (need 2+ options including current)
|
|
832
|
+
var currentSuffix = prefixes[base].suffix;
|
|
833
|
+
var currentOpt = { label: currentSuffix, className: currentClass, removeClassNames: [], pseudoState: null, pseudoStyles: null };
|
|
834
|
+
if (sizeOpts.length > 0) {
|
|
835
|
+
sizeOpts.unshift(currentOpt);
|
|
836
|
+
// Deduplicate
|
|
837
|
+
var seen = {};
|
|
838
|
+
sizeOpts = sizeOpts.filter(function(o) { if (seen[o.className]) return false; seen[o.className] = true; return true; });
|
|
839
|
+
if (sizeOpts.length >= 2) {
|
|
840
|
+
for (var soi = 0; soi < sizeOpts.length; soi++) { sizeOpts[soi].removeClassNames = [currentClass]; }
|
|
841
|
+
sizeOpts[0].removeClassNames = [];
|
|
842
|
+
groups.push({ groupName: 'Size', type: 'class', options: sizeOpts, activeIndex: 0 });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (colorOpts.length > 0) {
|
|
846
|
+
colorOpts.unshift(currentOpt);
|
|
847
|
+
var seenC = {};
|
|
848
|
+
colorOpts = colorOpts.filter(function(o) { if (seenC[o.className]) return false; seenC[o.className] = true; return true; });
|
|
849
|
+
if (colorOpts.length >= 2) {
|
|
850
|
+
for (var coi = 0; coi < colorOpts.length; coi++) { colorOpts[coi].removeClassNames = [currentClass]; }
|
|
851
|
+
colorOpts[0].removeClassNames = [];
|
|
852
|
+
groups.push({ groupName: 'Color', type: 'class', options: colorOpts, activeIndex: 0 });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (stateOpts.length > 0) {
|
|
856
|
+
stateOpts.unshift(currentOpt);
|
|
857
|
+
var seenS = {};
|
|
858
|
+
stateOpts = stateOpts.filter(function(o) { if (seenS[o.className]) return false; seenS[o.className] = true; return true; });
|
|
859
|
+
if (stateOpts.length >= 2) {
|
|
860
|
+
for (var stoi = 0; stoi < stateOpts.length; stoi++) { stateOpts[stoi].removeClassNames = [currentClass]; }
|
|
861
|
+
stateOpts[0].removeClassNames = [];
|
|
862
|
+
groups.push({ groupName: 'State', type: 'class', options: stateOpts, activeIndex: 0 });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return groups;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function detectPseudoVariants(el) {
|
|
870
|
+
try {
|
|
871
|
+
var visualProps = ['color','backgroundColor','borderColor','opacity','transform','boxShadow','textDecoration','outline'];
|
|
872
|
+
var defaultStyles = window.getComputedStyle(el);
|
|
873
|
+
var pseudos = ['hover','focus','active'];
|
|
874
|
+
var options = [{ label: 'default', className: null, removeClassNames: null, pseudoState: null, pseudoStyles: null }];
|
|
875
|
+
for (var pi = 0; pi < pseudos.length; pi++) {
|
|
876
|
+
var pseudo = pseudos[pi];
|
|
877
|
+
var pseudoStyles = window.getComputedStyle(el, ':' + pseudo);
|
|
878
|
+
var diffs = {};
|
|
879
|
+
var hasDiff = false;
|
|
880
|
+
for (var vi = 0; vi < visualProps.length; vi++) {
|
|
881
|
+
var prop = visualProps[vi];
|
|
882
|
+
var defaultVal = defaultStyles.getPropertyValue(prop);
|
|
883
|
+
var pseudoVal = pseudoStyles.getPropertyValue(prop);
|
|
884
|
+
if (defaultVal !== pseudoVal && pseudoVal) {
|
|
885
|
+
diffs[prop] = pseudoVal;
|
|
886
|
+
hasDiff = true;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (hasDiff) {
|
|
890
|
+
options.push({ label: pseudo, className: null, removeClassNames: null, pseudoState: pseudo, pseudoStyles: diffs });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (options.length >= 2) {
|
|
894
|
+
return [{ groupName: 'Pseudo States', type: 'pseudo', options: options, activeIndex: 0 }];
|
|
895
|
+
}
|
|
896
|
+
} catch(e) {}
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function scanForComponents(rootElement) {
|
|
901
|
+
var allElements = rootElement.querySelectorAll('*');
|
|
902
|
+
var results = [];
|
|
903
|
+
var batchSize = 50;
|
|
904
|
+
var index = 0;
|
|
905
|
+
var scheduleNext = typeof requestIdleCallback === 'function'
|
|
906
|
+
? function(cb) { requestIdleCallback(cb); }
|
|
907
|
+
: function(cb) { setTimeout(cb, 0); };
|
|
908
|
+
|
|
909
|
+
function processBatch() {
|
|
910
|
+
var end = Math.min(index + batchSize, allElements.length);
|
|
911
|
+
for (var i = index; i < end; i++) {
|
|
912
|
+
var el = allElements[i];
|
|
913
|
+
var detection = detectSingleComponent(el);
|
|
914
|
+
if (detection) {
|
|
915
|
+
var rect = el.getBoundingClientRect();
|
|
916
|
+
var text = (el.innerText || '').substring(0, 50) || null;
|
|
917
|
+
// Count child components
|
|
918
|
+
var childCount = 0;
|
|
919
|
+
var childEls = el.querySelectorAll('*');
|
|
920
|
+
for (var ci = 0; ci < childEls.length; ci++) {
|
|
921
|
+
if (detectSingleComponent(childEls[ci])) childCount++;
|
|
922
|
+
}
|
|
923
|
+
results.push({
|
|
924
|
+
selectorPath: generateSelectorPath(el),
|
|
925
|
+
name: contextualName(el, detection.name),
|
|
926
|
+
tagName: el.tagName.toLowerCase(),
|
|
927
|
+
detectionMethod: detection.method,
|
|
928
|
+
className: el.className && typeof el.className === 'string' ? el.className : null,
|
|
929
|
+
elementId: el.id || null,
|
|
930
|
+
innerText: text,
|
|
931
|
+
boundingRect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
|
|
932
|
+
variants: detectClassVariants(el).concat(detectPseudoVariants(el)),
|
|
933
|
+
childComponentCount: childCount
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
index = end;
|
|
938
|
+
if (index < allElements.length) {
|
|
939
|
+
scheduleNext(processBatch);
|
|
940
|
+
} else {
|
|
941
|
+
send({ type: 'COMPONENTS_DETECTED', payload: { components: results } });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (allElements.length === 0) {
|
|
945
|
+
send({ type: 'COMPONENTS_DETECTED', payload: { components: [] } });
|
|
946
|
+
} else {
|
|
947
|
+
processBatch();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// MutationObserver for DOM changes — throttled to at most once per animation frame
|
|
952
|
+
var mutationPending = false;
|
|
953
|
+
var previousTreeJSON = '';
|
|
954
|
+
var observer = new MutationObserver(function() {
|
|
955
|
+
if (mutationPending) return;
|
|
956
|
+
mutationPending = true;
|
|
957
|
+
requestAnimationFrame(function() {
|
|
958
|
+
mutationPending = false;
|
|
959
|
+
var tree = serializeTree(document.body);
|
|
960
|
+
if (!tree) return;
|
|
961
|
+
// Skip sending if tree hasn't changed (avoids redundant postMessages)
|
|
962
|
+
var treeJSON = JSON.stringify(tree);
|
|
963
|
+
if (treeJSON === previousTreeJSON) return;
|
|
964
|
+
previousTreeJSON = treeJSON;
|
|
965
|
+
var removedSelectors = [];
|
|
966
|
+
if (selectedElement && !document.body.contains(selectedElement)) {
|
|
967
|
+
removedSelectors.push(generateSelectorPath(selectedElement));
|
|
968
|
+
clearSelection();
|
|
969
|
+
}
|
|
970
|
+
send({ type: 'DOM_UPDATED', payload: { tree: tree, removedSelectors: removedSelectors } });
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'id', 'style'] });
|
|
974
|
+
|
|
975
|
+
// --- Drag-and-drop from Add Element palette ---
|
|
976
|
+
var VOID_TAGS_DND = {img:1,input:1,br:1,hr:1,area:1,base:1,col:1,embed:1,link:1,meta:1,param:1,source:1,track:1,wbr:1};
|
|
977
|
+
var dropIndicator = document.createElement('div');
|
|
978
|
+
dropIndicator.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483645;border:2px dashed #4a9eff;background:rgba(74,158,255,0.08);display:none;transition:top 0.08s,left 0.08s,width 0.08s,height 0.08s;';
|
|
979
|
+
document.body.appendChild(dropIndicator);
|
|
980
|
+
|
|
981
|
+
document.addEventListener('dragover', function(e) {
|
|
982
|
+
if (!e.dataTransfer || !e.dataTransfer.types.indexOf) return;
|
|
983
|
+
if (e.dataTransfer.types.indexOf('application/x-dev-editor-element') === -1) return;
|
|
984
|
+
e.preventDefault();
|
|
985
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
986
|
+
var dropTarget = document.elementFromPoint(e.clientX, e.clientY);
|
|
987
|
+
if (!dropTarget || dropTarget === dropIndicator) return;
|
|
988
|
+
// Skip inspector overlays
|
|
989
|
+
if (dropTarget.id && dropTarget.id.indexOf('dev-editor') === 0) return;
|
|
990
|
+
var rect = dropTarget.getBoundingClientRect();
|
|
991
|
+
dropIndicator.style.top = rect.top + 'px';
|
|
992
|
+
dropIndicator.style.left = rect.left + 'px';
|
|
993
|
+
dropIndicator.style.width = rect.width + 'px';
|
|
994
|
+
dropIndicator.style.height = rect.height + 'px';
|
|
995
|
+
dropIndicator.style.display = 'block';
|
|
996
|
+
}, true);
|
|
997
|
+
|
|
998
|
+
document.addEventListener('dragleave', function(e) {
|
|
999
|
+
if (e.relatedTarget === null || e.relatedTarget === document.documentElement) {
|
|
1000
|
+
dropIndicator.style.display = 'none';
|
|
1001
|
+
}
|
|
1002
|
+
}, true);
|
|
1003
|
+
|
|
1004
|
+
document.addEventListener('drop', function(e) {
|
|
1005
|
+
dropIndicator.style.display = 'none';
|
|
1006
|
+
if (!e.dataTransfer) return;
|
|
1007
|
+
var raw = e.dataTransfer.getData('application/x-dev-editor-element');
|
|
1008
|
+
if (!raw) return;
|
|
1009
|
+
e.preventDefault();
|
|
1010
|
+
e.stopPropagation();
|
|
1011
|
+
try {
|
|
1012
|
+
var data = JSON.parse(raw);
|
|
1013
|
+
var dropEl = document.elementFromPoint(e.clientX, e.clientY);
|
|
1014
|
+
if (!dropEl) return;
|
|
1015
|
+
// Skip inspector overlays
|
|
1016
|
+
if (dropEl.id && dropEl.id.indexOf('dev-editor') === 0) return;
|
|
1017
|
+
var targetParent = dropEl;
|
|
1018
|
+
var insertMode = 'child';
|
|
1019
|
+
if (VOID_TAGS_DND[dropEl.tagName.toLowerCase()]) {
|
|
1020
|
+
targetParent = dropEl.parentElement || document.body;
|
|
1021
|
+
insertMode = 'after';
|
|
1022
|
+
}
|
|
1023
|
+
var newEl = document.createElement(data.tag);
|
|
1024
|
+
newEl.setAttribute('data-dev-editor-inserted', 'true');
|
|
1025
|
+
if (data.placeholderText) {
|
|
1026
|
+
newEl.textContent = data.placeholderText;
|
|
1027
|
+
}
|
|
1028
|
+
if (data.defaultStyles) {
|
|
1029
|
+
var dndDS = data.defaultStyles;
|
|
1030
|
+
for (var dndDSKey in dndDS) {
|
|
1031
|
+
if (dndDS.hasOwnProperty(dndDSKey)) newEl.style.setProperty(dndDSKey, dndDS[dndDSKey]);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (insertMode === 'after' && dropEl.nextSibling) {
|
|
1035
|
+
targetParent.insertBefore(newEl, dropEl.nextSibling);
|
|
1036
|
+
} else {
|
|
1037
|
+
targetParent.appendChild(newEl);
|
|
1038
|
+
}
|
|
1039
|
+
var newSelector = generateSelectorPath(newEl);
|
|
1040
|
+
var newIndex = Array.from(targetParent.children).indexOf(newEl);
|
|
1041
|
+
send({
|
|
1042
|
+
type: 'ELEMENT_INSERTED',
|
|
1043
|
+
payload: {
|
|
1044
|
+
selectorPath: newSelector,
|
|
1045
|
+
parentSelectorPath: generateSelectorPath(targetParent),
|
|
1046
|
+
tagName: data.tag,
|
|
1047
|
+
insertionIndex: newIndex,
|
|
1048
|
+
placeholderText: data.placeholderText || '',
|
|
1049
|
+
defaultStyles: data.defaultStyles || undefined
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
} catch(err) {}
|
|
1053
|
+
}, true);
|
|
1054
|
+
|
|
1055
|
+
// Handle messages from editor
|
|
1056
|
+
window.addEventListener('message', function(e) {
|
|
1057
|
+
if (e.origin !== parentOrigin) return;
|
|
1058
|
+
var msg = e.data;
|
|
1059
|
+
if (!msg || !msg.type) return;
|
|
1060
|
+
|
|
1061
|
+
switch (msg.type) {
|
|
1062
|
+
case 'SELECT_ELEMENT': {
|
|
1063
|
+
try {
|
|
1064
|
+
var el = document.querySelector(msg.payload.selectorPath);
|
|
1065
|
+
if (el) {
|
|
1066
|
+
selectElement(el);
|
|
1067
|
+
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1068
|
+
}
|
|
1069
|
+
} catch(err) {}
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
case 'PREVIEW_CHANGE': {
|
|
1073
|
+
try {
|
|
1074
|
+
var target = document.querySelector(msg.payload.selectorPath);
|
|
1075
|
+
if (target) {
|
|
1076
|
+
var cssProp = toKC(msg.payload.property);
|
|
1077
|
+
requestAnimationFrame(function() {
|
|
1078
|
+
target.style.setProperty(cssProp, msg.payload.value, 'important');
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
} catch(err) {}
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
case 'REVERT_CHANGE': {
|
|
1085
|
+
try {
|
|
1086
|
+
var target2 = document.querySelector(msg.payload.selectorPath);
|
|
1087
|
+
if (target2) target2.style.removeProperty(toKC(msg.payload.property));
|
|
1088
|
+
} catch(err) {}
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
case 'REVERT_ALL': {
|
|
1092
|
+
var allElements = document.querySelectorAll('[style]');
|
|
1093
|
+
allElements.forEach(function(el) { el.removeAttribute('style'); });
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
case 'SET_SELECTION_MODE': {
|
|
1097
|
+
selectionModeEnabled = !!msg.payload.enabled;
|
|
1098
|
+
if (!selectionModeEnabled) {
|
|
1099
|
+
selectionOverlay.style.display = 'none';
|
|
1100
|
+
hoverOverlay.style.display = 'none';
|
|
1101
|
+
hoveredElement = null;
|
|
1102
|
+
selectedElement = null;
|
|
1103
|
+
}
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
case 'HIDE_HOVER': {
|
|
1107
|
+
hoverOverlay.style.display = 'none';
|
|
1108
|
+
hoveredElement = null;
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
case 'HIDE_SELECTION_OVERLAY': {
|
|
1112
|
+
selectionOverlay.style.display = 'none';
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
case 'SHOW_SELECTION_OVERLAY': {
|
|
1116
|
+
if (selectionModeEnabled && selectedElement) {
|
|
1117
|
+
selectionOverlay.style.display = 'block';
|
|
1118
|
+
}
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case 'SET_BREAKPOINT': {
|
|
1122
|
+
// Viewport controller handled externally
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
case 'REQUEST_DOM_TREE': {
|
|
1126
|
+
var tree = serializeTree(document.body);
|
|
1127
|
+
if (tree) send({ type: 'DOM_TREE', payload: { tree: tree } });
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
case 'REQUEST_PAGE_LINKS': {
|
|
1131
|
+
var links = [];
|
|
1132
|
+
var seen = {};
|
|
1133
|
+
var anchors = document.querySelectorAll('a[href]');
|
|
1134
|
+
for (var ai = 0; ai < anchors.length; ai++) {
|
|
1135
|
+
var rawHref = anchors[ai].getAttribute('href') || '';
|
|
1136
|
+
var linkText = (anchors[ai].textContent || '').trim();
|
|
1137
|
+
// Resolve to absolute URL
|
|
1138
|
+
var resolved;
|
|
1139
|
+
try { resolved = new URL(rawHref, window.location.origin); } catch(e) { continue; }
|
|
1140
|
+
// Only same-origin links
|
|
1141
|
+
if (resolved.origin !== window.location.origin) continue;
|
|
1142
|
+
var linkPath = resolved.pathname;
|
|
1143
|
+
// Strip /api/proxy prefix added by proxy rewriting
|
|
1144
|
+
if (linkPath.indexOf('/api/proxy') === 0) {
|
|
1145
|
+
linkPath = linkPath.substring(10) || '/';
|
|
1146
|
+
}
|
|
1147
|
+
// Skip API routes and empty paths
|
|
1148
|
+
if (linkPath.indexOf('/api/') === 0 || linkPath === '') continue;
|
|
1149
|
+
// Skip anchors, mailto, etc.
|
|
1150
|
+
if (!linkPath.startsWith('/')) continue;
|
|
1151
|
+
if (seen[linkPath]) continue;
|
|
1152
|
+
seen[linkPath] = true;
|
|
1153
|
+
links.push({ href: linkPath, text: linkText });
|
|
1154
|
+
}
|
|
1155
|
+
send({ type: 'PAGE_LINKS', payload: { links: links } });
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
case 'REQUEST_CSS_VARIABLES': {
|
|
1159
|
+
var result = scanCSSVariableDefinitions();
|
|
1160
|
+
initialVarCount = Object.keys(result.definitions).length;
|
|
1161
|
+
console.log('[pAInt] CSS variable definitions found:', initialVarCount, result.definitions);
|
|
1162
|
+
send({ type: 'CSS_VARIABLES', payload: { definitions: result.definitions, isExplicit: result.isExplicit, scopes: result.scopes || [] } });
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
case 'REQUEST_COMPONENTS': {
|
|
1166
|
+
try {
|
|
1167
|
+
var compRoot = document.body;
|
|
1168
|
+
if (msg.payload && msg.payload.rootSelectorPath) {
|
|
1169
|
+
var rootEl = document.querySelector(msg.payload.rootSelectorPath);
|
|
1170
|
+
if (rootEl) compRoot = rootEl;
|
|
1171
|
+
}
|
|
1172
|
+
scanForComponents(compRoot);
|
|
1173
|
+
} catch(err) {}
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
case 'APPLY_VARIANT': {
|
|
1177
|
+
try {
|
|
1178
|
+
var avEl = document.querySelector(msg.payload.selectorPath);
|
|
1179
|
+
if (avEl) {
|
|
1180
|
+
if (msg.payload.type === 'class') {
|
|
1181
|
+
if (msg.payload.removeClassNames) {
|
|
1182
|
+
for (var rci = 0; rci < msg.payload.removeClassNames.length; rci++) {
|
|
1183
|
+
avEl.classList.remove(msg.payload.removeClassNames[rci]);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (msg.payload.addClassName) {
|
|
1187
|
+
avEl.classList.add(msg.payload.addClassName);
|
|
1188
|
+
}
|
|
1189
|
+
} else if (msg.payload.type === 'pseudo') {
|
|
1190
|
+
if (msg.payload.revertPseudo && msg.payload.pseudoStyles) {
|
|
1191
|
+
var revertKeys = Object.keys(msg.payload.pseudoStyles);
|
|
1192
|
+
for (var rki = 0; rki < revertKeys.length; rki++) {
|
|
1193
|
+
avEl.style.removeProperty(revertKeys[rki]);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (msg.payload.pseudoStyles && !msg.payload.revertPseudo) {
|
|
1197
|
+
var psKeys = Object.keys(msg.payload.pseudoStyles);
|
|
1198
|
+
for (var psi = 0; psi < psKeys.length; psi++) {
|
|
1199
|
+
avEl.style.setProperty(psKeys[psi], msg.payload.pseudoStyles[psKeys[psi]], 'important');
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
var avStyles = getComputedStylesForElement(avEl);
|
|
1204
|
+
var avVarUsages = detectCSSVariablesOnElement(avEl);
|
|
1205
|
+
var avRect = avEl.getBoundingClientRect();
|
|
1206
|
+
send({ type: 'VARIANT_APPLIED', payload: {
|
|
1207
|
+
selectorPath: msg.payload.selectorPath,
|
|
1208
|
+
computedStyles: avStyles,
|
|
1209
|
+
cssVariableUsages: avVarUsages,
|
|
1210
|
+
boundingRect: { top: avRect.top, left: avRect.left, width: avRect.width, height: avRect.height }
|
|
1211
|
+
}});
|
|
1212
|
+
}
|
|
1213
|
+
} catch(err) {}
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
case 'REVERT_VARIANT': {
|
|
1217
|
+
try {
|
|
1218
|
+
var rvEl = document.querySelector(msg.payload.selectorPath);
|
|
1219
|
+
if (rvEl) {
|
|
1220
|
+
if (msg.payload.removeClassName) {
|
|
1221
|
+
rvEl.classList.remove(msg.payload.removeClassName);
|
|
1222
|
+
}
|
|
1223
|
+
if (msg.payload.restoreClassName) {
|
|
1224
|
+
rvEl.classList.add(msg.payload.restoreClassName);
|
|
1225
|
+
}
|
|
1226
|
+
if (msg.payload.revertPseudo && msg.payload.pseudoProperties) {
|
|
1227
|
+
for (var rppi = 0; rppi < msg.payload.pseudoProperties.length; rppi++) {
|
|
1228
|
+
rvEl.style.removeProperty(msg.payload.pseudoProperties[rppi]);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
} catch(err) {}
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
case 'SET_TEXT_CONTENT': {
|
|
1236
|
+
try {
|
|
1237
|
+
var stEl = document.querySelector(msg.payload.selectorPath);
|
|
1238
|
+
if (stEl) stEl.textContent = msg.payload.text;
|
|
1239
|
+
} catch(err) {}
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
case 'REVERT_TEXT_CONTENT': {
|
|
1243
|
+
try {
|
|
1244
|
+
var rtEl = document.querySelector(msg.payload.selectorPath);
|
|
1245
|
+
if (rtEl) rtEl.textContent = msg.payload.originalText;
|
|
1246
|
+
} catch(err) {}
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
case 'REVERT_DELETE': {
|
|
1250
|
+
try {
|
|
1251
|
+
var rdEl = document.querySelector(msg.payload.selectorPath);
|
|
1252
|
+
if (rdEl) {
|
|
1253
|
+
rdEl.style.removeProperty('display');
|
|
1254
|
+
}
|
|
1255
|
+
} catch(err) {}
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
case 'INSERT_ELEMENT': {
|
|
1259
|
+
try {
|
|
1260
|
+
var ieParent = document.querySelector(msg.payload.parentSelectorPath);
|
|
1261
|
+
if (ieParent) {
|
|
1262
|
+
var VOID_TAGS = {img:1,input:1,br:1,hr:1,area:1,base:1,col:1,embed:1,link:1,meta:1,param:1,source:1,track:1,wbr:1};
|
|
1263
|
+
var ieTarget = ieParent;
|
|
1264
|
+
var ieInsertMode = 'child';
|
|
1265
|
+
if (VOID_TAGS[ieParent.tagName.toLowerCase()]) {
|
|
1266
|
+
ieTarget = ieParent.parentElement || document.body;
|
|
1267
|
+
ieInsertMode = 'after';
|
|
1268
|
+
}
|
|
1269
|
+
var ieNew = document.createElement(msg.payload.tagName);
|
|
1270
|
+
ieNew.setAttribute('data-dev-editor-inserted', 'true');
|
|
1271
|
+
if (msg.payload.placeholderText) {
|
|
1272
|
+
ieNew.textContent = msg.payload.placeholderText;
|
|
1273
|
+
}
|
|
1274
|
+
if (msg.payload.defaultStyles) {
|
|
1275
|
+
var ieDS = msg.payload.defaultStyles;
|
|
1276
|
+
for (var ieDSKey in ieDS) {
|
|
1277
|
+
if (ieDS.hasOwnProperty(ieDSKey)) ieNew.style.setProperty(ieDSKey, ieDS[ieDSKey]);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (ieInsertMode === 'after' && ieParent.nextSibling) {
|
|
1281
|
+
ieTarget.insertBefore(ieNew, ieParent.nextSibling);
|
|
1282
|
+
} else {
|
|
1283
|
+
ieTarget.appendChild(ieNew);
|
|
1284
|
+
}
|
|
1285
|
+
var ieSelector = generateSelectorPath(ieNew);
|
|
1286
|
+
var ieIndex = Array.from(ieTarget.children).indexOf(ieNew);
|
|
1287
|
+
send({
|
|
1288
|
+
type: 'ELEMENT_INSERTED',
|
|
1289
|
+
payload: {
|
|
1290
|
+
selectorPath: ieSelector,
|
|
1291
|
+
parentSelectorPath: generateSelectorPath(ieTarget),
|
|
1292
|
+
tagName: msg.payload.tagName,
|
|
1293
|
+
insertionIndex: ieIndex,
|
|
1294
|
+
placeholderText: msg.payload.placeholderText || '',
|
|
1295
|
+
defaultStyles: msg.payload.defaultStyles || undefined
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
} catch(err) {}
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
case 'REMOVE_INSERTED_ELEMENT': {
|
|
1303
|
+
try {
|
|
1304
|
+
var rieEl = document.querySelector(msg.payload.selectorPath);
|
|
1305
|
+
if (rieEl && rieEl.parentElement) {
|
|
1306
|
+
rieEl.parentElement.removeChild(rieEl);
|
|
1307
|
+
}
|
|
1308
|
+
} catch(err) {}
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
case 'MOVE_ELEMENT': {
|
|
1312
|
+
try {
|
|
1313
|
+
var meEl = document.querySelector(msg.payload.selectorPath);
|
|
1314
|
+
var meNewParent = document.querySelector(msg.payload.newParentSelectorPath);
|
|
1315
|
+
if (meEl && meNewParent && meEl !== meNewParent) {
|
|
1316
|
+
// Prevent moving an element into its own descendant
|
|
1317
|
+
if (meEl.contains(meNewParent)) break;
|
|
1318
|
+
var meOldParent = meEl.parentElement;
|
|
1319
|
+
var meOldIndex = meOldParent ? Array.from(meOldParent.children).indexOf(meEl) : -1;
|
|
1320
|
+
var meOldParentSelector = meOldParent ? generateSelectorPath(meOldParent) : '';
|
|
1321
|
+
// Remove from current position
|
|
1322
|
+
if (meOldParent) meOldParent.removeChild(meEl);
|
|
1323
|
+
// Insert at new position
|
|
1324
|
+
var meNewIndex = msg.payload.newIndex;
|
|
1325
|
+
var meChildren = meNewParent.children;
|
|
1326
|
+
if (meNewIndex >= 0 && meNewIndex < meChildren.length) {
|
|
1327
|
+
meNewParent.insertBefore(meEl, meChildren[meNewIndex]);
|
|
1328
|
+
} else {
|
|
1329
|
+
meNewParent.appendChild(meEl);
|
|
1330
|
+
}
|
|
1331
|
+
var meNewSelector = generateSelectorPath(meEl);
|
|
1332
|
+
var meActualIndex = Array.from(meNewParent.children).indexOf(meEl);
|
|
1333
|
+
send({
|
|
1334
|
+
type: 'ELEMENT_MOVED',
|
|
1335
|
+
payload: {
|
|
1336
|
+
selectorPath: msg.payload.selectorPath,
|
|
1337
|
+
newSelectorPath: meNewSelector,
|
|
1338
|
+
oldParentSelectorPath: meOldParentSelector,
|
|
1339
|
+
newParentSelectorPath: msg.payload.newParentSelectorPath,
|
|
1340
|
+
oldIndex: meOldIndex,
|
|
1341
|
+
newIndex: meActualIndex
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
if (selectedElement === meEl) {
|
|
1345
|
+
updateOverlays();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
} catch(err) {}
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
case 'REVERT_MOVE_ELEMENT': {
|
|
1352
|
+
try {
|
|
1353
|
+
var rmEl = document.querySelector(msg.payload.selectorPath);
|
|
1354
|
+
var rmOldParent = document.querySelector(msg.payload.oldParentSelectorPath);
|
|
1355
|
+
if (rmEl && rmOldParent) {
|
|
1356
|
+
var rmCurrentParent = rmEl.parentElement;
|
|
1357
|
+
if (rmCurrentParent) rmCurrentParent.removeChild(rmEl);
|
|
1358
|
+
var rmOldIndex = msg.payload.oldIndex;
|
|
1359
|
+
var rmChildren = rmOldParent.children;
|
|
1360
|
+
if (rmOldIndex >= 0 && rmOldIndex < rmChildren.length) {
|
|
1361
|
+
rmOldParent.insertBefore(rmEl, rmChildren[rmOldIndex]);
|
|
1362
|
+
} else {
|
|
1363
|
+
rmOldParent.appendChild(rmEl);
|
|
1364
|
+
}
|
|
1365
|
+
if (selectedElement === rmEl) {
|
|
1366
|
+
updateOverlays();
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
} catch(err) {}
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
case 'HEARTBEAT': {
|
|
1373
|
+
send({ type: 'HEARTBEAT_RESPONSE' });
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
case 'NAVIGATE_TO': {
|
|
1377
|
+
var navPath = msg.payload.path || '/';
|
|
1378
|
+
var navSep = (navPath.indexOf('?') >= 0) ? '&' : '?';
|
|
1379
|
+
window.location.href = '/api/proxy' + navPath + navSep + pH + '=' + eT;
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// --- Animation reveal ---
|
|
1386
|
+
// Animation libraries (Framer Motion, GSAP) set elements to opacity:0 +
|
|
1387
|
+
// translateY via inline styles as initial state, then animate on scroll/mount.
|
|
1388
|
+
// In the proxy context, these animations may not fire. Detect and fix.
|
|
1389
|
+
function revealAnimationHidden() {
|
|
1390
|
+
var els = document.querySelectorAll('[style]');
|
|
1391
|
+
for (var ri = 0; ri < els.length; ri++) {
|
|
1392
|
+
var rel = els[ri];
|
|
1393
|
+
var rst = rel.getAttribute('style') || '';
|
|
1394
|
+
if (!/opacity\\s*:\\s*0\\b/.test(rst)) continue;
|
|
1395
|
+
var rcs = window.getComputedStyle(rel);
|
|
1396
|
+
if (rcs.display === 'none' || rcs.visibility === 'hidden') continue;
|
|
1397
|
+
rel.style.setProperty('transition', 'opacity 0.3s ease, transform 0.3s ease', 'important');
|
|
1398
|
+
rel.style.setProperty('opacity', '1', 'important');
|
|
1399
|
+
if (/translate[YX]\\s*\\(/.test(rst) || /matrix\\s*\\(/.test(rcs.transform)) {
|
|
1400
|
+
rel.style.setProperty('transform', 'none', 'important');
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
var revealAttempts = 0;
|
|
1405
|
+
var revealTimer = setInterval(function() {
|
|
1406
|
+
revealAnimationHidden();
|
|
1407
|
+
revealAttempts++;
|
|
1408
|
+
if (revealAttempts >= 5) clearInterval(revealTimer);
|
|
1409
|
+
}, 800);
|
|
1410
|
+
|
|
1411
|
+
// Signal ready
|
|
1412
|
+
if (document.readyState === 'loading') {
|
|
1413
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1414
|
+
send({ type: 'INSPECTOR_READY' });
|
|
1415
|
+
});
|
|
1416
|
+
} else {
|
|
1417
|
+
send({ type: 'INSPECTOR_READY' });
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Re-scan CSS variables after all resources (stylesheets, fonts) have loaded.
|
|
1421
|
+
// The initial scan on INSPECTOR_READY may miss variables from stylesheets
|
|
1422
|
+
// that haven't fully loaded yet (async CSS, @import chains, JS-injected styles).
|
|
1423
|
+
var initialVarCount = -1;
|
|
1424
|
+
function rescanCSSVariables() {
|
|
1425
|
+
var result = scanCSSVariableDefinitions();
|
|
1426
|
+
var count = Object.keys(result.definitions).length;
|
|
1427
|
+
if (count > initialVarCount) {
|
|
1428
|
+
initialVarCount = count;
|
|
1429
|
+
send({ type: 'CSS_VARIABLES', payload: { definitions: result.definitions, isExplicit: result.isExplicit, scopes: result.scopes || [] } });
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
window.addEventListener('load', function() {
|
|
1433
|
+
setTimeout(rescanCSSVariables, 300);
|
|
1434
|
+
});
|
|
1435
|
+
// Also re-scan after a longer delay for JS-injected styles (React hydration, etc.)
|
|
1436
|
+
setTimeout(rescanCSSVariables, 3000);
|
|
1437
|
+
|
|
1438
|
+
return { selectElement: selectElement, clearSelection: clearSelection };
|
|
1439
|
+
})();
|
|
1440
|
+
`
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function isLocalhostUrl(url: string): boolean {
|
|
1444
|
+
try {
|
|
1445
|
+
const parsed = new URL(url)
|
|
1446
|
+
return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'
|
|
1447
|
+
} catch {
|
|
1448
|
+
return false
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
async function handleProxy(request: NextRequest, params: { path?: string[] }) {
|
|
1453
|
+
// Accept target URL from header, query param, or cookie (for dynamic chunk loading)
|
|
1454
|
+
const targetUrl =
|
|
1455
|
+
request.headers.get(PROXY_HEADER) ||
|
|
1456
|
+
request.nextUrl.searchParams.get(PROXY_HEADER) ||
|
|
1457
|
+
request.cookies.get(PROXY_HEADER)?.value
|
|
1458
|
+
|
|
1459
|
+
if (!targetUrl) {
|
|
1460
|
+
return NextResponse.json(
|
|
1461
|
+
{ error: 'Missing X-Dev-Editor-Target header or query parameter' },
|
|
1462
|
+
{ status: 400 },
|
|
1463
|
+
)
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (!isLocalhostUrl(targetUrl)) {
|
|
1467
|
+
return NextResponse.json(
|
|
1468
|
+
{ error: 'Target URL must be localhost or 127.0.0.1' },
|
|
1469
|
+
{ status: 400 },
|
|
1470
|
+
)
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const path = (params.path || []).join('/')
|
|
1474
|
+
|
|
1475
|
+
// Short-circuit HMR requests — return empty responses so webpack's
|
|
1476
|
+
// hot-update polling and HMR connections stop at the proxy instead of
|
|
1477
|
+
// hitting the target server (which returns 404s and triggers error loops).
|
|
1478
|
+
if (
|
|
1479
|
+
path.includes('.hot-update.') ||
|
|
1480
|
+
path.includes('webpack-hmr') ||
|
|
1481
|
+
path.includes('__turbopack_hmr') ||
|
|
1482
|
+
path.includes('turbopack-hmr')
|
|
1483
|
+
) {
|
|
1484
|
+
// For JSON manifests, return empty object (no updates available)
|
|
1485
|
+
if (path.endsWith('.json')) {
|
|
1486
|
+
return new NextResponse('{}', {
|
|
1487
|
+
status: 200,
|
|
1488
|
+
headers: { 'content-type': 'application/json' },
|
|
1489
|
+
})
|
|
1490
|
+
}
|
|
1491
|
+
// For JS hot-update chunks, return empty script
|
|
1492
|
+
if (path.endsWith('.js')) {
|
|
1493
|
+
return new NextResponse('', {
|
|
1494
|
+
status: 200,
|
|
1495
|
+
headers: { 'content-type': 'application/javascript' },
|
|
1496
|
+
})
|
|
1497
|
+
}
|
|
1498
|
+
// Anything else HMR-related: empty 204
|
|
1499
|
+
return new NextResponse(null, { status: 204 })
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const url = new URL(path || '/', targetUrl)
|
|
1503
|
+
|
|
1504
|
+
// Forward query string (excluding the proxy header param)
|
|
1505
|
+
request.nextUrl.searchParams.forEach((value, key) => {
|
|
1506
|
+
if (key !== PROXY_HEADER) {
|
|
1507
|
+
url.searchParams.set(key, value)
|
|
1508
|
+
}
|
|
1509
|
+
})
|
|
1510
|
+
|
|
1511
|
+
try {
|
|
1512
|
+
const controller = new AbortController()
|
|
1513
|
+
const timeout = setTimeout(() => controller.abort(), 10000)
|
|
1514
|
+
|
|
1515
|
+
const headers = new Headers()
|
|
1516
|
+
request.headers.forEach((value, key) => {
|
|
1517
|
+
if (
|
|
1518
|
+
key !== PROXY_HEADER &&
|
|
1519
|
+
key !== 'host' &&
|
|
1520
|
+
!key.startsWith('x-forwarded') &&
|
|
1521
|
+
key !== 'connection' &&
|
|
1522
|
+
key !== 'accept-encoding'
|
|
1523
|
+
) {
|
|
1524
|
+
headers.set(key, value)
|
|
1525
|
+
}
|
|
1526
|
+
})
|
|
1527
|
+
// Request uncompressed responses from the target. The proxy streams
|
|
1528
|
+
// non-HTML/CSS bodies directly and strips content-encoding, so
|
|
1529
|
+
// receiving compressed bytes would corrupt JS chunks, fonts, etc.
|
|
1530
|
+
headers.set('accept-encoding', 'identity')
|
|
1531
|
+
|
|
1532
|
+
const response = await fetch(url.toString(), {
|
|
1533
|
+
method: request.method,
|
|
1534
|
+
headers,
|
|
1535
|
+
body:
|
|
1536
|
+
request.method !== 'GET' && request.method !== 'HEAD'
|
|
1537
|
+
? await request.arrayBuffer()
|
|
1538
|
+
: undefined,
|
|
1539
|
+
signal: controller.signal,
|
|
1540
|
+
redirect: 'manual',
|
|
1541
|
+
})
|
|
1542
|
+
|
|
1543
|
+
clearTimeout(timeout)
|
|
1544
|
+
|
|
1545
|
+
const contentType = response.headers.get('content-type') || ''
|
|
1546
|
+
// Headers to strip from proxied responses:
|
|
1547
|
+
// - content-encoding/transfer-encoding: proxy re-encodes the body
|
|
1548
|
+
// - COEP/COOP/CORP: block inspector script and iframe embedding
|
|
1549
|
+
// - CSP: may restrict inline scripts (inspector) and postMessage
|
|
1550
|
+
// - X-Frame-Options: prevents iframe embedding of proxied pages
|
|
1551
|
+
const STRIP_HEADERS = new Set([
|
|
1552
|
+
'content-encoding',
|
|
1553
|
+
'transfer-encoding',
|
|
1554
|
+
'cross-origin-embedder-policy',
|
|
1555
|
+
'cross-origin-opener-policy',
|
|
1556
|
+
'cross-origin-resource-policy',
|
|
1557
|
+
'content-security-policy',
|
|
1558
|
+
'content-security-policy-report-only',
|
|
1559
|
+
'x-frame-options',
|
|
1560
|
+
])
|
|
1561
|
+
|
|
1562
|
+
// Handle redirects from the target (e.g. auth middleware redirecting to /login).
|
|
1563
|
+
// Rewrite the Location header to go through the proxy so the browser stays
|
|
1564
|
+
// within the iframe and cookies/auth state are preserved.
|
|
1565
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1566
|
+
const location = response.headers.get('location')
|
|
1567
|
+
if (location) {
|
|
1568
|
+
let rewrittenLocation = location
|
|
1569
|
+
const targetOriginForRedirect = new URL(targetUrl).origin
|
|
1570
|
+
try {
|
|
1571
|
+
const locUrl = new URL(location, targetUrl)
|
|
1572
|
+
// Only rewrite same-origin redirects (to the target server)
|
|
1573
|
+
if (locUrl.origin === targetOriginForRedirect) {
|
|
1574
|
+
const sep = locUrl.search ? '&' : '?'
|
|
1575
|
+
rewrittenLocation = `/api/proxy${locUrl.pathname}${locUrl.search}${sep}${PROXY_HEADER}=${encodeURIComponent(targetUrl)}${locUrl.hash}`
|
|
1576
|
+
}
|
|
1577
|
+
} catch {
|
|
1578
|
+
// If URL parsing fails, pass through as-is
|
|
1579
|
+
}
|
|
1580
|
+
const redirectHeaders = new Headers()
|
|
1581
|
+
response.headers.forEach((value, key) => {
|
|
1582
|
+
if (!STRIP_HEADERS.has(key) && key !== 'location') {
|
|
1583
|
+
if (key === 'set-cookie') {
|
|
1584
|
+
redirectHeaders.append(key, value)
|
|
1585
|
+
} else {
|
|
1586
|
+
redirectHeaders.set(key, value)
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
})
|
|
1590
|
+
redirectHeaders.set('location', rewrittenLocation)
|
|
1591
|
+
return new NextResponse(null, {
|
|
1592
|
+
status: response.status,
|
|
1593
|
+
headers: redirectHeaders,
|
|
1594
|
+
})
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
const responseHeaders = new Headers()
|
|
1598
|
+
response.headers.forEach((value, key) => {
|
|
1599
|
+
if (!STRIP_HEADERS.has(key)) {
|
|
1600
|
+
// Use append for set-cookie to preserve multiple cookies (e.g. Supabase
|
|
1601
|
+
// auth sends separate access-token, refresh-token, etc. cookies).
|
|
1602
|
+
// Using set() would overwrite all but the last cookie.
|
|
1603
|
+
if (key === 'set-cookie') {
|
|
1604
|
+
responseHeaders.append(key, value)
|
|
1605
|
+
} else {
|
|
1606
|
+
responseHeaders.set(key, value)
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
})
|
|
1610
|
+
|
|
1611
|
+
// Inject inspector script into HTML responses
|
|
1612
|
+
if (contentType.includes('text/html')) {
|
|
1613
|
+
let html = await response.text()
|
|
1614
|
+
|
|
1615
|
+
// Strip any existing pAInt inspector scripts the target app may
|
|
1616
|
+
// have added manually. The proxy injects its own inspector, so these
|
|
1617
|
+
// duplicates cause multiple overlays and conflicting message handlers.
|
|
1618
|
+
html = html.replace(
|
|
1619
|
+
/<script[^>]*src=["'][^"']*dev-editor-inspector\.js["'][^>]*><\/script>/gi,
|
|
1620
|
+
'',
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
// Rewrite asset URLs to go through proxy, preserving target param
|
|
1624
|
+
const encodedTarget = encodeURIComponent(targetUrl)
|
|
1625
|
+
const targetOrigin = new URL(targetUrl).origin
|
|
1626
|
+
const escapedOrigin = targetOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1627
|
+
|
|
1628
|
+
// Helper to rewrite a single absolute path, preserving URL fragments
|
|
1629
|
+
function proxyPath(originalPath: string): string {
|
|
1630
|
+
let pathPart = originalPath
|
|
1631
|
+
let fragment = ''
|
|
1632
|
+
const hashIdx = originalPath.indexOf('#')
|
|
1633
|
+
if (hashIdx >= 0) {
|
|
1634
|
+
pathPart = originalPath.substring(0, hashIdx)
|
|
1635
|
+
fragment = originalPath.substring(hashIdx)
|
|
1636
|
+
}
|
|
1637
|
+
const separator = pathPart.includes('?') ? '&' : '?'
|
|
1638
|
+
return `/api/proxy${pathPart}${separator}${PROXY_HEADER}=${encodedTarget}${fragment}`
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Rewrite fully-qualified target-origin URLs in attributes FIRST
|
|
1642
|
+
// (e.g., http://localhost:4000/avatars/img.png → /api/proxy/avatars/img.png?...)
|
|
1643
|
+
html = html.replace(
|
|
1644
|
+
new RegExp(
|
|
1645
|
+
`(href|src|action|poster)=(["'])${escapedOrigin}(/[^"']*)`,
|
|
1646
|
+
'g',
|
|
1647
|
+
),
|
|
1648
|
+
(_match: string, attr: string, quote: string, pathPart: string) => {
|
|
1649
|
+
// Tag /_next/ paths with _devproxy marker so middleware can identify
|
|
1650
|
+
// them as iframe-originated after history.replaceState changes the referer.
|
|
1651
|
+
// Do NOT add /api/proxy/ prefix — that breaks Turbopack chunk path matching.
|
|
1652
|
+
if (pathPart.startsWith('/_next/')) {
|
|
1653
|
+
const sep = pathPart.includes('?') ? '&' : '?'
|
|
1654
|
+
return `${attr}=${quote}${pathPart}${sep}_devproxy=1`
|
|
1655
|
+
}
|
|
1656
|
+
return `${attr}=${quote}${proxyPath(pathPart)}`
|
|
1657
|
+
},
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
// Rewrite fully-qualified target-origin URLs in srcset
|
|
1661
|
+
html = html.replace(
|
|
1662
|
+
new RegExp(`${escapedOrigin}(/[^\\s,)"']+)`, 'g'),
|
|
1663
|
+
(_match: string, pathPart: string) => {
|
|
1664
|
+
// Only rewrite inside attribute contexts (not in script text)
|
|
1665
|
+
return proxyPath(pathPart)
|
|
1666
|
+
},
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
// Rewrite src, href, action, poster attributes (absolute paths starting with /)
|
|
1670
|
+
// Tag /_next/ paths with ?_devproxy=1 so the middleware can proxy them to
|
|
1671
|
+
// the target server. Without this marker, after history.replaceState changes
|
|
1672
|
+
// the URL, the referer no longer contains /api/proxy and the middleware
|
|
1673
|
+
// skips the request — loading pAInt's own chunks instead of the
|
|
1674
|
+
// target's, which prevents React from hydrating.
|
|
1675
|
+
// Do NOT add /api/proxy/ prefix — that breaks Turbopack chunk path matching.
|
|
1676
|
+
// Turbopack's getPathFromScript() strips query strings, so the marker is safe.
|
|
1677
|
+
html = html.replace(
|
|
1678
|
+
/(href|src|action|poster)=(["'])(\/[^"']*)/g,
|
|
1679
|
+
(_match: string, attr: string, quote: string, originalPath: string) => {
|
|
1680
|
+
// Skip already-rewritten paths
|
|
1681
|
+
if (originalPath.startsWith('/api/proxy')) return _match
|
|
1682
|
+
// Tag /_next/ paths with _devproxy marker for middleware identification
|
|
1683
|
+
if (originalPath.startsWith('/_next/')) {
|
|
1684
|
+
if (originalPath.includes('_devproxy=')) return _match
|
|
1685
|
+
const sep = originalPath.includes('?') ? '&' : '?'
|
|
1686
|
+
return `${attr}=${quote}${originalPath}${sep}_devproxy=1`
|
|
1687
|
+
}
|
|
1688
|
+
return `${attr}=${quote}${proxyPath(originalPath)}`
|
|
1689
|
+
},
|
|
1690
|
+
)
|
|
1691
|
+
|
|
1692
|
+
// Rewrite xlink:href for SVG <use> elements (absolute paths starting with /)
|
|
1693
|
+
html = html.replace(
|
|
1694
|
+
/xlink:href=(["'])(\/[^"']*)/g,
|
|
1695
|
+
(_match: string, quote: string, originalPath: string) => {
|
|
1696
|
+
if (originalPath.startsWith('/api/proxy')) return _match
|
|
1697
|
+
return `xlink:href=${quote}${proxyPath(originalPath)}`
|
|
1698
|
+
},
|
|
1699
|
+
)
|
|
1700
|
+
|
|
1701
|
+
// Rewrite srcset attributes — each entry is "url descriptor, ..."
|
|
1702
|
+
html = html.replace(
|
|
1703
|
+
/srcset=(["'])([^"']+)/g,
|
|
1704
|
+
(_match: string, quote: string, srcsetValue: string) => {
|
|
1705
|
+
const rewritten = srcsetValue.replace(
|
|
1706
|
+
/(\/[^\s,]+)/g,
|
|
1707
|
+
(urlPart: string) => {
|
|
1708
|
+
if (urlPart.startsWith('/api/proxy')) return urlPart
|
|
1709
|
+
return proxyPath(urlPart)
|
|
1710
|
+
},
|
|
1711
|
+
)
|
|
1712
|
+
return `srcset=${quote}${rewritten}`
|
|
1713
|
+
},
|
|
1714
|
+
)
|
|
1715
|
+
|
|
1716
|
+
// Rewrite data-src and data-srcset (common lazy-loading patterns)
|
|
1717
|
+
html = html.replace(
|
|
1718
|
+
/data-src=(["'])(\/[^"']*)/g,
|
|
1719
|
+
(_match: string, quote: string, originalPath: string) => {
|
|
1720
|
+
if (originalPath.startsWith('/api/proxy')) return _match
|
|
1721
|
+
return `data-src=${quote}${proxyPath(originalPath)}`
|
|
1722
|
+
},
|
|
1723
|
+
)
|
|
1724
|
+
html = html.replace(
|
|
1725
|
+
/data-srcset=(["'])([^"']+)/g,
|
|
1726
|
+
(_match: string, quote: string, srcsetValue: string) => {
|
|
1727
|
+
const rewritten = srcsetValue.replace(
|
|
1728
|
+
/(\/[^\s,]+)/g,
|
|
1729
|
+
(urlPart: string) => {
|
|
1730
|
+
if (urlPart.startsWith('/api/proxy')) return urlPart
|
|
1731
|
+
return proxyPath(urlPart)
|
|
1732
|
+
},
|
|
1733
|
+
)
|
|
1734
|
+
return `data-srcset=${quote}${rewritten}`
|
|
1735
|
+
},
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
// Rewrite CSS url() references in inline styles — both absolute paths and full URLs
|
|
1739
|
+
html = html.replace(
|
|
1740
|
+
/url\((["']?)(\/[^)"']+)\1\)/g,
|
|
1741
|
+
(_match: string, quote: string, originalPath: string) => {
|
|
1742
|
+
if (originalPath.startsWith('/api/proxy')) return _match
|
|
1743
|
+
return `url(${quote}${proxyPath(originalPath)}${quote})`
|
|
1744
|
+
},
|
|
1745
|
+
)
|
|
1746
|
+
html = html.replace(
|
|
1747
|
+
new RegExp(`url\\((["']?)${escapedOrigin}(/[^)"']+)\\1\\)`, 'g'),
|
|
1748
|
+
(_match: string, quote: string, pathPart: string) => {
|
|
1749
|
+
return `url(${quote}${proxyPath(pathPart)}${quote})`
|
|
1750
|
+
},
|
|
1751
|
+
)
|
|
1752
|
+
|
|
1753
|
+
// Rewrite @import in <style> blocks
|
|
1754
|
+
html = html.replace(
|
|
1755
|
+
/@import\s+(["'])(\/[^"']+)\1/g,
|
|
1756
|
+
(_match: string, quote: string, originalPath: string) => {
|
|
1757
|
+
return `@import ${quote}${proxyPath(originalPath)}${quote}`
|
|
1758
|
+
},
|
|
1759
|
+
)
|
|
1760
|
+
|
|
1761
|
+
// --- Strip target-page scripts to prevent hydration failures ---
|
|
1762
|
+
// Remove ALL <script> tags from the target HTML except:
|
|
1763
|
+
// - type="application/ld+json" (structured data / JSON-LD)
|
|
1764
|
+
// The SSR-rendered HTML + CSS is sufficient for visual editing.
|
|
1765
|
+
// Allowing target scripts to execute causes Next.js hydration to
|
|
1766
|
+
// run, fail (missing chunks like GSAP, layout.js → 404), and wipe
|
|
1767
|
+
// out the pre-rendered content entirely — leaving a blank page.
|
|
1768
|
+
// Our own scripts (nav blocker, interceptor, inspector) are injected
|
|
1769
|
+
// AFTER this stripping step, so they are not affected.
|
|
1770
|
+
html = html.replace(
|
|
1771
|
+
/<script\b[^>]*>[\s\S]*?<\/script>/gi,
|
|
1772
|
+
(match: string) => {
|
|
1773
|
+
if (/type\s*=\s*["']application\/ld\+json["']/i.test(match)) {
|
|
1774
|
+
return match
|
|
1775
|
+
}
|
|
1776
|
+
return ''
|
|
1777
|
+
},
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
// --- Navigation blocker ---
|
|
1781
|
+
// Even with target scripts stripped, we inject a navigation blocker that:
|
|
1782
|
+
// 1. Fixes the URL via history.replaceState so the inspector reports
|
|
1783
|
+
// correct paths (not /api/proxy/...)
|
|
1784
|
+
// 2. Rewrites dynamically-set resource URLs through the proxy
|
|
1785
|
+
// 3. Intercepts any remaining navigations to keep the iframe in the proxy
|
|
1786
|
+
// 4. Patches fetch/XHR in case the inspector or other injected scripts
|
|
1787
|
+
// need to make requests through the proxy
|
|
1788
|
+
// 5. Suppresses HMR-related errors as a safety net
|
|
1789
|
+
// 6. Detects infinite reload loops as a safety net
|
|
1790
|
+
const targetPagePath = '/' + (path || '')
|
|
1791
|
+
const safePagePath = JSON.stringify(targetPagePath)
|
|
1792
|
+
const safeTargetUrl = JSON.stringify(targetUrl)
|
|
1793
|
+
const safeEncodedTarget = JSON.stringify(encodedTarget)
|
|
1794
|
+
const safeProxyHeader = JSON.stringify(PROXY_HEADER)
|
|
1795
|
+
|
|
1796
|
+
const navigationBlockerScript = `<script data-dev-editor-nav-blocker>
|
|
1797
|
+
(function(){
|
|
1798
|
+
var tP=${safePagePath},tU=${safeTargetUrl},eT=${safeEncodedTarget},pH=${safeProxyHeader};
|
|
1799
|
+
|
|
1800
|
+
// Fix URL so client-side routers see the correct path
|
|
1801
|
+
try {
|
|
1802
|
+
var p = new URLSearchParams(window.location.search);
|
|
1803
|
+
p.delete(pH);
|
|
1804
|
+
var qs = p.toString();
|
|
1805
|
+
history.replaceState(history.state, '', tP + (qs ? '?' + qs : ''));
|
|
1806
|
+
} catch(e) {}
|
|
1807
|
+
|
|
1808
|
+
// Block duplicate inspector scripts — the proxy injects its own inline
|
|
1809
|
+
// inspector, so any external dev-editor-inspector.js must be suppressed.
|
|
1810
|
+
// Intercept HTMLScriptElement.src setter to prevent dynamic loading.
|
|
1811
|
+
var scrDesc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src');
|
|
1812
|
+
if (scrDesc && scrDesc.set) {
|
|
1813
|
+
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
|
|
1814
|
+
get: scrDesc.get,
|
|
1815
|
+
set: function(val) {
|
|
1816
|
+
if (typeof val === 'string' && val.indexOf('dev-editor-inspector') >= 0) return;
|
|
1817
|
+
if (typeof val === 'string') {
|
|
1818
|
+
var r = proxyResUrl(val);
|
|
1819
|
+
if (r) val = r;
|
|
1820
|
+
}
|
|
1821
|
+
scrDesc.set.call(this, val);
|
|
1822
|
+
},
|
|
1823
|
+
configurable: true, enumerable: true
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
// Reload safety net - detect infinite reload loops
|
|
1827
|
+
var rk = '_der';
|
|
1828
|
+
var rc = parseInt(sessionStorage.getItem(rk) || '0');
|
|
1829
|
+
sessionStorage.setItem(rk, String(rc + 1));
|
|
1830
|
+
setTimeout(function(){ sessionStorage.removeItem(rk); }, 3000);
|
|
1831
|
+
if (rc > 4) { sessionStorage.removeItem(rk); window.stop(); return; }
|
|
1832
|
+
|
|
1833
|
+
// HMR isolation: return alive-looking WebSocket/EventSource mocks for HMR
|
|
1834
|
+
// connections. Turbopack bootstrapping does NOT require the HMR WebSocket —
|
|
1835
|
+
// initial module loading happens via <script> tags. The HMR connection is
|
|
1836
|
+
// only for live updates. Routing it to the target server causes CORS failures.
|
|
1837
|
+
// IMPORTANT: Mocks must report readyState=1 (OPEN) and fire the 'open' event
|
|
1838
|
+
// so the HMR client believes it's connected. Previous mocks reported CLOSED
|
|
1839
|
+
// and fired close/error, causing the HMR client to enter a reconnection loop
|
|
1840
|
+
// that eventually triggered window.location.reload() every ~40 seconds.
|
|
1841
|
+
var OWS = window.WebSocket;
|
|
1842
|
+
window.WebSocket = function(u, pr) {
|
|
1843
|
+
var s = String(u);
|
|
1844
|
+
if (s.indexOf('_next') >= 0 || s.indexOf('hmr') >= 0 || s.indexOf('webpack') >= 0 || s.indexOf('turbopack') >= 0 || s.indexOf('hot-update') >= 0) {
|
|
1845
|
+
var _wl = {};
|
|
1846
|
+
var m = {
|
|
1847
|
+
readyState: 1,
|
|
1848
|
+
bufferedAmount: 0, extensions: '', protocol: '', url: s, binaryType: 'blob',
|
|
1849
|
+
close: function(){ m.readyState = 3; },
|
|
1850
|
+
send: function(){},
|
|
1851
|
+
addEventListener: function(t, fn){ if(!_wl[t]) _wl[t]=[]; _wl[t].push(fn); },
|
|
1852
|
+
removeEventListener: function(t, fn){ if(_wl[t]) _wl[t]=_wl[t].filter(function(f){return f!==fn;}); },
|
|
1853
|
+
dispatchEvent: function(){ return true; }
|
|
1854
|
+
};
|
|
1855
|
+
setTimeout(function(){
|
|
1856
|
+
var ev = {type:'open'};
|
|
1857
|
+
if (m.onopen) try { m.onopen(ev); } catch(e) {}
|
|
1858
|
+
if (_wl.open) for (var i=0; i<_wl.open.length; i++) try { _wl.open[i](ev); } catch(e) {}
|
|
1859
|
+
}, 10);
|
|
1860
|
+
return m;
|
|
1861
|
+
}
|
|
1862
|
+
return pr !== undefined ? new OWS(u, pr) : new OWS(u);
|
|
1863
|
+
};
|
|
1864
|
+
window.WebSocket.CONNECTING=0; window.WebSocket.OPEN=1; window.WebSocket.CLOSING=2; window.WebSocket.CLOSED=3;
|
|
1865
|
+
|
|
1866
|
+
var OES = window.EventSource;
|
|
1867
|
+
if (OES) {
|
|
1868
|
+
window.EventSource = function(u, c) {
|
|
1869
|
+
var s = String(u);
|
|
1870
|
+
if (s.indexOf('hmr') >= 0 || s.indexOf('hot') >= 0 || s.indexOf('turbopack') >= 0 || s.indexOf('webpack') >= 0 || s.indexOf('_next') >= 0) {
|
|
1871
|
+
var _el = {};
|
|
1872
|
+
var m = {
|
|
1873
|
+
readyState: 1, url: s, withCredentials: false,
|
|
1874
|
+
close: function(){ m.readyState = 2; },
|
|
1875
|
+
addEventListener: function(t, fn){ if(!_el[t]) _el[t]=[]; _el[t].push(fn); },
|
|
1876
|
+
removeEventListener: function(t, fn){ if(_el[t]) _el[t]=_el[t].filter(function(f){return f!==fn;}); },
|
|
1877
|
+
dispatchEvent: function(){ return true; }
|
|
1878
|
+
};
|
|
1879
|
+
setTimeout(function(){
|
|
1880
|
+
var ev = {type:'open'};
|
|
1881
|
+
if (m.onopen) try { m.onopen(ev); } catch(e) {}
|
|
1882
|
+
if (_el.open) for (var i=0; i<_el.open.length; i++) try { _el.open[i](ev); } catch(e) {}
|
|
1883
|
+
}, 10);
|
|
1884
|
+
return m;
|
|
1885
|
+
}
|
|
1886
|
+
return c ? new OES(u, c) : new OES(u);
|
|
1887
|
+
};
|
|
1888
|
+
window.EventSource.CONNECTING=0; window.EventSource.OPEN=1; window.EventSource.CLOSED=2;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// Intercept navigations via Navigation API to prevent the iframe from
|
|
1892
|
+
// escaping the proxy. After history.replaceState changes the URL to the
|
|
1893
|
+
// target path (e.g. http://localhost:4000/), any unintercepted navigation
|
|
1894
|
+
// would load pAInt's own page instead of the target — causing
|
|
1895
|
+
// the "recursive embed" bug where the setup modal appears inside the iframe.
|
|
1896
|
+
if (window.navigation) {
|
|
1897
|
+
window.navigation.addEventListener('navigate', function(e) {
|
|
1898
|
+
if (e.hashChange) return;
|
|
1899
|
+
try {
|
|
1900
|
+
var d = new URL(e.destination.url);
|
|
1901
|
+
// Already going through proxy — allow
|
|
1902
|
+
if (d.pathname.indexOf('/api/proxy') === 0) return;
|
|
1903
|
+
// Cross-origin navigation — allow (external links)
|
|
1904
|
+
if (d.origin !== window.location.origin) return;
|
|
1905
|
+
// Same-origin, not through proxy — must intercept to prevent
|
|
1906
|
+
// loading pAInt's own page (recursive embed)
|
|
1907
|
+
if (e.canIntercept) {
|
|
1908
|
+
e.intercept({
|
|
1909
|
+
handler: function() {
|
|
1910
|
+
// User-initiated (link click, form submit) or reload:
|
|
1911
|
+
// redirect through the proxy so the target page loads correctly
|
|
1912
|
+
if (e.userInitiated || e.navigationType === 'reload') {
|
|
1913
|
+
if (e.userInitiated) {
|
|
1914
|
+
window.parent.postMessage({type:'PAGE_NAVIGATE', payload:{path:d.pathname}}, window.location.origin);
|
|
1915
|
+
}
|
|
1916
|
+
var sep = d.search ? '&' : '?';
|
|
1917
|
+
window.location.replace('/api/proxy' + d.pathname + d.search + sep + pH + '=' + eT);
|
|
1918
|
+
return new Promise(function() {});
|
|
1919
|
+
}
|
|
1920
|
+
// Programmatic push/replace (SPA router internal navigation):
|
|
1921
|
+
// resolve immediately without full navigation to prevent
|
|
1922
|
+
// infinite reload loops while keeping the router functional
|
|
1923
|
+
return Promise.resolve();
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
} catch(err) {}
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// Patch fetch for same-origin AND target-origin API calls
|
|
1932
|
+
var oF = window.fetch;
|
|
1933
|
+
var tO = new URL(tU).origin;
|
|
1934
|
+
function rewriteUrl(s) {
|
|
1935
|
+
if (typeof s !== 'string') return s;
|
|
1936
|
+
// Relative paths starting with /
|
|
1937
|
+
if (s.charAt(0) === '/' && s.indexOf('/api/proxy') !== 0) {
|
|
1938
|
+
return '/api/proxy' + s + (s.indexOf('?') >= 0 ? '&' : '?') + pH + '=' + eT;
|
|
1939
|
+
}
|
|
1940
|
+
// Fully-qualified target-origin URLs (e.g. http://localhost:4000/api/data)
|
|
1941
|
+
if (s.indexOf(tO) === 0) {
|
|
1942
|
+
var path = s.substring(tO.length) || '/';
|
|
1943
|
+
return '/api/proxy' + path + (path.indexOf('?') >= 0 ? '&' : '?') + pH + '=' + eT;
|
|
1944
|
+
}
|
|
1945
|
+
return s;
|
|
1946
|
+
}
|
|
1947
|
+
window.fetch = function(i, n) {
|
|
1948
|
+
try {
|
|
1949
|
+
if (typeof i === 'string') {
|
|
1950
|
+
i = rewriteUrl(i);
|
|
1951
|
+
} else if (typeof Request !== 'undefined' && i instanceof Request) {
|
|
1952
|
+
var u = new URL(i.url);
|
|
1953
|
+
if ((u.origin === window.location.origin && u.pathname.indexOf('/api/proxy') !== 0) || u.origin === tO) {
|
|
1954
|
+
var rp = u.origin === tO ? u.pathname : u.pathname;
|
|
1955
|
+
i = new Request('/api/proxy' + rp + u.search + (u.search ? '&' : '?') + pH + '=' + eT, i);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
} catch(e) {}
|
|
1959
|
+
return oF.call(this, i, n);
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
// Patch XMLHttpRequest for same-origin AND target-origin calls
|
|
1963
|
+
var oX = XMLHttpRequest.prototype.open;
|
|
1964
|
+
XMLHttpRequest.prototype.open = function(m, u) {
|
|
1965
|
+
try {
|
|
1966
|
+
if (typeof u === 'string') {
|
|
1967
|
+
arguments[1] = rewriteUrl(u);
|
|
1968
|
+
}
|
|
1969
|
+
} catch(e) {}
|
|
1970
|
+
return oX.apply(this, arguments);
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
// Synchronous property interceptors — rewrite URLs BEFORE the browser
|
|
1974
|
+
// starts fetching resources. MutationObserver is async (too late for images).
|
|
1975
|
+
// All interceptors use proxyResUrl() which handles fragments correctly.
|
|
1976
|
+
// Patch Element.prototype.setAttribute to catch attr-based URL setting.
|
|
1977
|
+
var oSA = Element.prototype.setAttribute;
|
|
1978
|
+
Element.prototype.setAttribute = function(name, value) {
|
|
1979
|
+
if (typeof value === 'string') {
|
|
1980
|
+
var n = name.toLowerCase();
|
|
1981
|
+
// Block duplicate inspector scripts set via setAttribute
|
|
1982
|
+
if (n === 'src' && this.tagName === 'SCRIPT' && value.indexOf('dev-editor-inspector') >= 0) {
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (n === 'src' || n === 'poster' || n === 'data-src' || (n === 'href' && this.tagName !== 'A')) {
|
|
1986
|
+
var r = proxyResUrl(value);
|
|
1987
|
+
if (r) value = r;
|
|
1988
|
+
}
|
|
1989
|
+
if (n === 'srcset') {
|
|
1990
|
+
var parts = value.split(',');
|
|
1991
|
+
var rewritten = [];
|
|
1992
|
+
for (var si = 0; si < parts.length; si++) {
|
|
1993
|
+
var entry = parts[si].trim();
|
|
1994
|
+
var spIdx = entry.indexOf(' ');
|
|
1995
|
+
var url = spIdx >= 0 ? entry.substring(0, spIdx) : entry;
|
|
1996
|
+
var desc = spIdx >= 0 ? entry.substring(spIdx) : '';
|
|
1997
|
+
var ru = proxyResUrl(url);
|
|
1998
|
+
rewritten.push((ru || url) + desc);
|
|
1999
|
+
}
|
|
2000
|
+
value = rewritten.join(', ');
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return oSA.call(this, name, value);
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
// Patch .src property on HTMLImageElement — React sets img.src directly
|
|
2007
|
+
var imgDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');
|
|
2008
|
+
if (imgDesc && imgDesc.set) {
|
|
2009
|
+
Object.defineProperty(HTMLImageElement.prototype, 'src', {
|
|
2010
|
+
get: imgDesc.get,
|
|
2011
|
+
set: function(val) {
|
|
2012
|
+
var r = proxyResUrl(val);
|
|
2013
|
+
imgDesc.set.call(this, r || val);
|
|
2014
|
+
},
|
|
2015
|
+
configurable: true, enumerable: true
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Patch .src on HTMLSourceElement (for <source> in <picture>/<video>)
|
|
2020
|
+
var srcDesc = Object.getOwnPropertyDescriptor(HTMLSourceElement.prototype, 'src');
|
|
2021
|
+
if (srcDesc && srcDesc.set) {
|
|
2022
|
+
Object.defineProperty(HTMLSourceElement.prototype, 'src', {
|
|
2023
|
+
get: srcDesc.get,
|
|
2024
|
+
set: function(val) {
|
|
2025
|
+
var r = proxyResUrl(val);
|
|
2026
|
+
srcDesc.set.call(this, r || val);
|
|
2027
|
+
},
|
|
2028
|
+
configurable: true, enumerable: true
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Intercept FontFace constructor — Expo/React Native Web and other frameworks
|
|
2033
|
+
// load icon fonts (Ionicons, Material Icons, etc.) via new FontFace('name', 'url(...)')
|
|
2034
|
+
// instead of <style> elements. Without this, the font URL bypasses the proxy.
|
|
2035
|
+
var OFontFace = window.FontFace;
|
|
2036
|
+
if (OFontFace) {
|
|
2037
|
+
window.FontFace = function(family, source, descriptors) {
|
|
2038
|
+
if (typeof source === 'string') {
|
|
2039
|
+
source = source.replace(/url\\(\\s*(['"]?)([^)'"\\s]+)\\1\\s*\\)/g, function(m, q, urlVal) {
|
|
2040
|
+
var r = proxyResUrl(urlVal);
|
|
2041
|
+
return r ? 'url(' + q + r + q + ')' : m;
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
return new OFontFace(family, source, descriptors);
|
|
2045
|
+
};
|
|
2046
|
+
window.FontFace.prototype = OFontFace.prototype;
|
|
2047
|
+
// Preserve static methods if any
|
|
2048
|
+
Object.keys(OFontFace).forEach(function(k) {
|
|
2049
|
+
try { window.FontFace[k] = OFontFace[k]; } catch(e) {}
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Rewrite url() references in dynamically-injected <style> elements
|
|
2054
|
+
// (e.g., icon font CSS injected by Font Awesome, Material Icons, etc.)
|
|
2055
|
+
var _processedStyles = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
|
|
2056
|
+
function rewriteStyleUrls(styleEl) {
|
|
2057
|
+
if (_processedStyles) {
|
|
2058
|
+
if (_processedStyles.has(styleEl)) return;
|
|
2059
|
+
_processedStyles.add(styleEl);
|
|
2060
|
+
}
|
|
2061
|
+
var css = styleEl.textContent;
|
|
2062
|
+
if (!css || css.indexOf('url(') < 0) return;
|
|
2063
|
+
var newCss = css.replace(/url\\(\\s*(['"]?)([^)'"\\s]+)\\1\\s*\\)/g, function(m, q, urlVal) {
|
|
2064
|
+
var r = proxyResUrl(urlVal);
|
|
2065
|
+
return r ? 'url(' + q + r + q + ')' : m;
|
|
2066
|
+
});
|
|
2067
|
+
if (newCss !== css) styleEl.textContent = newCss;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// Runtime interceptor: rewrite src/href on dynamically-created elements
|
|
2071
|
+
// and url() in <style> elements (icon fonts, etc.)
|
|
2072
|
+
var rObs = new MutationObserver(function(mutations) {
|
|
2073
|
+
for (var mi = 0; mi < mutations.length; mi++) {
|
|
2074
|
+
var added = mutations[mi].addedNodes;
|
|
2075
|
+
for (var ni = 0; ni < added.length; ni++) {
|
|
2076
|
+
var node = added[ni];
|
|
2077
|
+
// Text node added inside <style> (textContent change on icon font CSS)
|
|
2078
|
+
if (node.nodeType === 3 && node.parentElement && node.parentElement.tagName === 'STYLE') {
|
|
2079
|
+
rewriteStyleUrls(node.parentElement);
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
if (node.nodeType !== 1) continue;
|
|
2083
|
+
// <style> element with @font-face or background-image url() references
|
|
2084
|
+
if (node.tagName === 'STYLE') {
|
|
2085
|
+
rewriteStyleUrls(node);
|
|
2086
|
+
}
|
|
2087
|
+
rewriteNodeUrls(node);
|
|
2088
|
+
if (node.querySelectorAll) {
|
|
2089
|
+
var children = node.querySelectorAll('[src],[href],[poster],[data-src],[srcset]');
|
|
2090
|
+
for (var ci = 0; ci < children.length; ci++) rewriteNodeUrls(children[ci]);
|
|
2091
|
+
// Also rewrite url() in <style> elements within the added subtree
|
|
2092
|
+
var styles = node.querySelectorAll('style');
|
|
2093
|
+
for (var sti = 0; sti < styles.length; sti++) rewriteStyleUrls(styles[sti]);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
// Handle attribute changes (e.g., lazy-load libraries setting src)
|
|
2097
|
+
if (mutations[mi].type === 'attributes') {
|
|
2098
|
+
rewriteNodeUrls(mutations[mi].target);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
// Rewrite a resource URL to go through proxy. Returns null if no rewrite needed.
|
|
2103
|
+
// Handles URL fragments (#id) by placing them after the query string.
|
|
2104
|
+
function proxyResUrl(val) {
|
|
2105
|
+
if (!val || typeof val !== 'string') return null;
|
|
2106
|
+
if (val.indexOf('/api/proxy') === 0) return null;
|
|
2107
|
+
if (val.indexOf('data:') === 0 || val.indexOf('blob:') === 0 || val.charAt(0) === '#' || val.indexOf('javascript:') === 0) return null;
|
|
2108
|
+
// Mark /_next/ paths with ?_devproxy=1 so the middleware can identify them as
|
|
2109
|
+
// iframe-originated requests. We must NOT add the /api/proxy/ prefix
|
|
2110
|
+
// because that corrupts turbopack's getPathFromScript() chunk path matching.
|
|
2111
|
+
// The middleware strips the query string before proxying. Turbopack's
|
|
2112
|
+
// getPathFromScript also strips query strings, so path matching still works.
|
|
2113
|
+
// NOTE: Do NOT use "_dp" — Next.js uses ?_dp=1 internally for CSS preloading.
|
|
2114
|
+
if (val.indexOf('/_next/') === 0) {
|
|
2115
|
+
if (val.indexOf('_devproxy=') >= 0) return null; // Already marked
|
|
2116
|
+
return val + (val.indexOf('?') >= 0 ? '&' : '?') + '_devproxy=1';
|
|
2117
|
+
}
|
|
2118
|
+
var fragment = '';
|
|
2119
|
+
var hashIdx = val.indexOf('#');
|
|
2120
|
+
var urlPart = val;
|
|
2121
|
+
if (hashIdx >= 0) { urlPart = val.substring(0, hashIdx); fragment = val.substring(hashIdx); }
|
|
2122
|
+
var proxied = null;
|
|
2123
|
+
if (urlPart.indexOf(tO) === 0) {
|
|
2124
|
+
proxied = '/api/proxy' + (urlPart.substring(tO.length) || '/');
|
|
2125
|
+
} else if (urlPart.charAt(0) === '/') {
|
|
2126
|
+
proxied = '/api/proxy' + urlPart;
|
|
2127
|
+
}
|
|
2128
|
+
if (proxied) {
|
|
2129
|
+
return proxied + (proxied.indexOf('?') >= 0 ? '&' : '?') + pH + '=' + eT + fragment;
|
|
2130
|
+
}
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
function rewriteNodeUrls(el) {
|
|
2134
|
+
if (!el || !el.getAttribute) return;
|
|
2135
|
+
var tag = el.tagName;
|
|
2136
|
+
// Resource attributes that need proxy rewriting
|
|
2137
|
+
var attrs = ['src', 'poster', 'data-src'];
|
|
2138
|
+
// Rewrite href for non-anchor elements (stylesheets, etc.) but NOT <a> tags
|
|
2139
|
+
// (anchor clicks are handled by the Navigation API intercept above)
|
|
2140
|
+
if (tag !== 'A') attrs.push('href');
|
|
2141
|
+
for (var ai = 0; ai < attrs.length; ai++) {
|
|
2142
|
+
var val = el.getAttribute(attrs[ai]);
|
|
2143
|
+
var r = proxyResUrl(val);
|
|
2144
|
+
if (r) el.setAttribute(attrs[ai], r);
|
|
2145
|
+
}
|
|
2146
|
+
// SVG xlink:href for <use> elements (e.g., <use xlink:href="/sprites.svg#icon">)
|
|
2147
|
+
if (el.getAttributeNS) {
|
|
2148
|
+
var xval = el.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
|
|
2149
|
+
var xr = proxyResUrl(xval);
|
|
2150
|
+
if (xr) el.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', xr);
|
|
2151
|
+
}
|
|
2152
|
+
// Handle srcset — split on comma, rewrite each entry
|
|
2153
|
+
var srcset = el.getAttribute('srcset');
|
|
2154
|
+
if (srcset) {
|
|
2155
|
+
var parts = srcset.split(',');
|
|
2156
|
+
var changed = false;
|
|
2157
|
+
var rewritten = [];
|
|
2158
|
+
for (var si = 0; si < parts.length; si++) {
|
|
2159
|
+
var entry = parts[si].trim();
|
|
2160
|
+
var spIdx = entry.indexOf(' ');
|
|
2161
|
+
var url = spIdx >= 0 ? entry.substring(0, spIdx) : entry;
|
|
2162
|
+
var desc = spIdx >= 0 ? entry.substring(spIdx) : '';
|
|
2163
|
+
var ru = proxyResUrl(url);
|
|
2164
|
+
if (ru) { rewritten.push(ru + desc); changed = true; }
|
|
2165
|
+
else rewritten.push(entry);
|
|
2166
|
+
}
|
|
2167
|
+
if (changed) {
|
|
2168
|
+
el.setAttribute('srcset', rewritten.join(', '));
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
// Scan existing <style> elements in <head> that may have been added before
|
|
2173
|
+
// the observer started (e.g., server-rendered @font-face or early JS injection)
|
|
2174
|
+
var existingStyles = document.querySelectorAll('head style, style');
|
|
2175
|
+
for (var esi = 0; esi < existingStyles.length; esi++) {
|
|
2176
|
+
rewriteStyleUrls(existingStyles[esi]);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// Observe document.documentElement (<html>) instead of just document.body
|
|
2180
|
+
// so we catch <style> elements with @font-face injected into <head> by
|
|
2181
|
+
// frameworks like Expo/React Native Web and icon font loaders.
|
|
2182
|
+
var obsRoot = document.documentElement || document.body;
|
|
2183
|
+
if (obsRoot) {
|
|
2184
|
+
rObs.observe(obsRoot, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'href', 'poster', 'data-src', 'srcset'] });
|
|
2185
|
+
} else {
|
|
2186
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
2187
|
+
rObs.observe(document.documentElement || document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'href', 'poster', 'data-src', 'srcset'] });
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// Suppress HMR and chunk-loading errors
|
|
2192
|
+
function isProxyNoise(s) {
|
|
2193
|
+
return s.indexOf('hmr') >= 0 || s.indexOf('hot') >= 0 || s.indexOf('WebSocket') >= 0 || s.indexOf('__webpack') >= 0 || s.indexOf('turbopack') >= 0 || s.indexOf('ChunkLoadError') >= 0 || s.indexOf('Loading chunk') >= 0 || s.indexOf('Loading CSS chunk') >= 0;
|
|
2194
|
+
}
|
|
2195
|
+
window.addEventListener('error', function(e) {
|
|
2196
|
+
if (isProxyNoise(e.message || '')) {
|
|
2197
|
+
e.stopImmediatePropagation(); e.preventDefault(); return false;
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
window.addEventListener('unhandledrejection', function(e) {
|
|
2201
|
+
if (isProxyNoise(e.reason ? String(e.reason) : '')) {
|
|
2202
|
+
e.stopImmediatePropagation(); e.preventDefault();
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
// Hide Next.js dev error overlay (uses <nextjs-portal> custom element)
|
|
2206
|
+
var hs=document.createElement('style');
|
|
2207
|
+
hs.textContent='nextjs-portal{display:none!important}';
|
|
2208
|
+
document.documentElement.appendChild(hs);
|
|
2209
|
+
})();
|
|
2210
|
+
</script>`
|
|
2211
|
+
|
|
2212
|
+
// Cookie setter for resource loading
|
|
2213
|
+
const urlInterceptorScript = `<script data-dev-editor-interceptor>
|
|
2214
|
+
(function(){
|
|
2215
|
+
document.cookie='${PROXY_HEADER}='+encodeURIComponent('${targetUrl}')+';path=/;SameSite=Strict;max-age=86400';
|
|
2216
|
+
})();
|
|
2217
|
+
</script>`
|
|
2218
|
+
|
|
2219
|
+
// Inject navigation blocker + cookie setter at the top of <head>.
|
|
2220
|
+
// IMPORTANT: Use function replacements to prevent $ characters in the
|
|
2221
|
+
// injected scripts from being interpreted as special replacement patterns
|
|
2222
|
+
// ($' = text after match, $& = matched text, etc.).
|
|
2223
|
+
const headInjection = navigationBlockerScript + urlInterceptorScript
|
|
2224
|
+
if (/<head>/i.test(html)) {
|
|
2225
|
+
html = html.replace(/<head>/i, (match) => match + headInjection)
|
|
2226
|
+
} else if (/<head\s/i.test(html)) {
|
|
2227
|
+
html = html.replace(/<head\s[^>]*>/i, (match) => match + headInjection)
|
|
2228
|
+
} else {
|
|
2229
|
+
html = headInjection + html
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// Set cookie on the response for dynamic resource loading
|
|
2233
|
+
responseHeaders.append(
|
|
2234
|
+
'Set-Cookie',
|
|
2235
|
+
`${PROXY_HEADER}=${encodeURIComponent(targetUrl)}; Path=/; SameSite=Strict; Max-Age=86400`,
|
|
2236
|
+
)
|
|
2237
|
+
|
|
2238
|
+
// Strip CSP meta tags that could block the inline inspector script
|
|
2239
|
+
html = html.replace(
|
|
2240
|
+
/<meta\s+http-equiv=["']?Content-Security-Policy["']?[^>]*>/gi,
|
|
2241
|
+
'',
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
// Inject inspector script before </body> (case-insensitive)
|
|
2245
|
+
if (/<\/body>/i.test(html)) {
|
|
2246
|
+
html = html.replace(/<\/body>/i, () => INSPECTOR_SCRIPT + '</body>')
|
|
2247
|
+
} else {
|
|
2248
|
+
html += INSPECTOR_SCRIPT
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
responseHeaders.set('content-type', 'text/html; charset=utf-8')
|
|
2252
|
+
responseHeaders.set(
|
|
2253
|
+
'cache-control',
|
|
2254
|
+
'no-cache, no-store, must-revalidate',
|
|
2255
|
+
)
|
|
2256
|
+
responseHeaders.delete('content-length')
|
|
2257
|
+
|
|
2258
|
+
return new NextResponse(html, {
|
|
2259
|
+
status: response.status,
|
|
2260
|
+
headers: responseHeaders,
|
|
2261
|
+
})
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// Rewrite url() references in CSS responses
|
|
2265
|
+
if (contentType.includes('text/css')) {
|
|
2266
|
+
let css = await response.text()
|
|
2267
|
+
const cssEncodedTarget = encodeURIComponent(targetUrl)
|
|
2268
|
+
const cssTargetOrigin = new URL(targetUrl).origin
|
|
2269
|
+
const cssEscapedOrigin = cssTargetOrigin.replace(
|
|
2270
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
2271
|
+
'\\$&',
|
|
2272
|
+
)
|
|
2273
|
+
// Rewrite absolute-path url() references
|
|
2274
|
+
css = css.replace(
|
|
2275
|
+
/url\(\s*(["']?)(\/[^)"'\s]+)\1\s*\)/g,
|
|
2276
|
+
(_match: string, quote: string, originalPath: string) => {
|
|
2277
|
+
if (originalPath.startsWith('/api/proxy')) return _match
|
|
2278
|
+
const separator = originalPath.includes('?') ? '&' : '?'
|
|
2279
|
+
return `url(${quote}/api/proxy${originalPath}${separator}${PROXY_HEADER}=${cssEncodedTarget}${quote})`
|
|
2280
|
+
},
|
|
2281
|
+
)
|
|
2282
|
+
// Rewrite fully-qualified target-origin url() references
|
|
2283
|
+
css = css.replace(
|
|
2284
|
+
new RegExp(
|
|
2285
|
+
`url\\(\\s*(["']?)${cssEscapedOrigin}(/[^)"'\\s]+)\\1\\s*\\)`,
|
|
2286
|
+
'g',
|
|
2287
|
+
),
|
|
2288
|
+
(_match: string, quote: string, pathPart: string) => {
|
|
2289
|
+
const separator = pathPart.includes('?') ? '&' : '?'
|
|
2290
|
+
return `url(${quote}/api/proxy${pathPart}${separator}${PROXY_HEADER}=${cssEncodedTarget}${quote})`
|
|
2291
|
+
},
|
|
2292
|
+
)
|
|
2293
|
+
// Rewrite @import with absolute paths
|
|
2294
|
+
css = css.replace(
|
|
2295
|
+
/@import\s+(["'])(\/[^"']+)\1/g,
|
|
2296
|
+
(_match: string, quote: string, originalPath: string) => {
|
|
2297
|
+
const separator = originalPath.includes('?') ? '&' : '?'
|
|
2298
|
+
return `@import ${quote}/api/proxy${originalPath}${separator}${PROXY_HEADER}=${cssEncodedTarget}${quote}`
|
|
2299
|
+
},
|
|
2300
|
+
)
|
|
2301
|
+
responseHeaders.set('content-type', 'text/css; charset=utf-8')
|
|
2302
|
+
responseHeaders.set(
|
|
2303
|
+
'cache-control',
|
|
2304
|
+
'no-cache, no-store, must-revalidate',
|
|
2305
|
+
)
|
|
2306
|
+
responseHeaders.delete('content-length')
|
|
2307
|
+
return new NextResponse(css, {
|
|
2308
|
+
status: response.status,
|
|
2309
|
+
headers: responseHeaders,
|
|
2310
|
+
})
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// Add CORS headers for fonts (often needed for cross-origin loading)
|
|
2314
|
+
if (
|
|
2315
|
+
contentType.includes('font/') ||
|
|
2316
|
+
contentType.includes('application/font') ||
|
|
2317
|
+
path.match(/\.(woff2?|ttf|eot|otf)(\?|$)/)
|
|
2318
|
+
) {
|
|
2319
|
+
responseHeaders.set('access-control-allow-origin', '*')
|
|
2320
|
+
responseHeaders.set(
|
|
2321
|
+
'cache-control',
|
|
2322
|
+
'public, max-age=31536000, immutable',
|
|
2323
|
+
)
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// Images: always revalidate so updated assets on the target are
|
|
2327
|
+
// reflected immediately instead of being served from browser cache.
|
|
2328
|
+
if (
|
|
2329
|
+
contentType.includes('image/') ||
|
|
2330
|
+
path.match(/\.(png|jpg|jpeg|gif|svg|ico|webp|avif)(\?|$)/)
|
|
2331
|
+
) {
|
|
2332
|
+
responseHeaders.set(
|
|
2333
|
+
'cache-control',
|
|
2334
|
+
'no-cache, no-store, must-revalidate',
|
|
2335
|
+
)
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// Passthrough other responses (streams body directly — no buffering)
|
|
2339
|
+
return new NextResponse(response.body, {
|
|
2340
|
+
status: response.status,
|
|
2341
|
+
headers: responseHeaders,
|
|
2342
|
+
})
|
|
2343
|
+
} catch (error) {
|
|
2344
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
2345
|
+
return NextResponse.json(
|
|
2346
|
+
{ error: 'Target server timeout (10s)' },
|
|
2347
|
+
{ status: 504 },
|
|
2348
|
+
)
|
|
2349
|
+
}
|
|
2350
|
+
return NextResponse.json(
|
|
2351
|
+
{ error: 'Target server is unreachable' },
|
|
2352
|
+
{ status: 502 },
|
|
2353
|
+
)
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
export async function GET(
|
|
2358
|
+
request: NextRequest,
|
|
2359
|
+
context: { params: Promise<{ path?: string[] }> },
|
|
2360
|
+
) {
|
|
2361
|
+
const params = await context.params
|
|
2362
|
+
return handleProxy(request, params)
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
export async function POST(
|
|
2366
|
+
request: NextRequest,
|
|
2367
|
+
context: { params: Promise<{ path?: string[] }> },
|
|
2368
|
+
) {
|
|
2369
|
+
const params = await context.params
|
|
2370
|
+
return handleProxy(request, params)
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
export async function PUT(
|
|
2374
|
+
request: NextRequest,
|
|
2375
|
+
context: { params: Promise<{ path?: string[] }> },
|
|
2376
|
+
) {
|
|
2377
|
+
const params = await context.params
|
|
2378
|
+
return handleProxy(request, params)
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
export async function DELETE(
|
|
2382
|
+
request: NextRequest,
|
|
2383
|
+
context: { params: Promise<{ path?: string[] }> },
|
|
2384
|
+
) {
|
|
2385
|
+
const params = await context.params
|
|
2386
|
+
return handleProxy(request, params)
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
export async function OPTIONS(request: NextRequest) {
|
|
2390
|
+
return new NextResponse(null, {
|
|
2391
|
+
status: 204,
|
|
2392
|
+
headers: {
|
|
2393
|
+
'access-control-allow-origin': '*',
|
|
2394
|
+
'access-control-allow-methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
2395
|
+
'access-control-allow-headers':
|
|
2396
|
+
request.headers.get('access-control-request-headers') || '*',
|
|
2397
|
+
'access-control-max-age': '86400',
|
|
2398
|
+
},
|
|
2399
|
+
})
|
|
2400
|
+
}
|