rlint 0.4.0 → 0.6.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/README.md +235 -32
- package/dist/checks/clickability.d.ts.map +1 -1
- package/dist/checks/clickability.js +65 -30
- package/dist/checks/clickability.js.map +1 -1
- package/dist/checks/index.d.ts +1 -0
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +5 -0
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/touch-targets.d.ts.map +1 -1
- package/dist/checks/touch-targets.js +134 -47
- package/dist/checks/touch-targets.js.map +1 -1
- package/dist/checks/viewport-meta.d.ts +3 -0
- package/dist/checks/viewport-meta.d.ts.map +1 -0
- package/dist/checks/viewport-meta.js +113 -0
- package/dist/checks/viewport-meta.js.map +1 -0
- package/dist/cli.js +179 -49
- package/dist/cli.js.map +1 -1
- package/dist/mcp-server.js +158 -31
- package/dist/mcp-server.js.map +1 -1
- package/dist/proxy/client-script.d.ts +9 -0
- package/dist/proxy/client-script.d.ts.map +1 -0
- package/dist/proxy/client-script.js +449 -0
- package/dist/proxy/client-script.js.map +1 -0
- package/dist/proxy/index.d.ts +4 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +4 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/result-cache.d.ts +46 -0
- package/dist/proxy/result-cache.d.ts.map +1 -0
- package/dist/proxy/result-cache.js +27 -0
- package/dist/proxy/result-cache.js.map +1 -0
- package/dist/proxy/server.d.ts +17 -0
- package/dist/proxy/server.d.ts.map +1 -0
- package/dist/proxy/server.js +190 -0
- package/dist/proxy/server.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the client-side JavaScript that gets injected into pages via the proxy.
|
|
3
|
+
* This script runs all rlint checks directly in the user's browser and POSTs
|
|
4
|
+
* results back to the proxy server.
|
|
5
|
+
*
|
|
6
|
+
* The check logic mirrors the page.evaluate() bodies in src/checks/*.ts.
|
|
7
|
+
*/
|
|
8
|
+
export function getClientScript() {
|
|
9
|
+
return `(function() {
|
|
10
|
+
'use strict';
|
|
11
|
+
if (window.__rlint__) return;
|
|
12
|
+
window.__rlint__ = { version: '0.6.0', results: null };
|
|
13
|
+
|
|
14
|
+
var ENDPOINT = '/__rlint__/results';
|
|
15
|
+
var IGNORE = ['[data-rlint-ignore]'];
|
|
16
|
+
var INTERACTIVE = ['button','a','input:not([type="hidden"])','select','textarea','[role="button"]','[role="link"]','[onclick]','[tabindex="0"]'];
|
|
17
|
+
|
|
18
|
+
// === Shared helpers ===
|
|
19
|
+
|
|
20
|
+
function makeSelector(el) {
|
|
21
|
+
var s = el.tagName.toLowerCase();
|
|
22
|
+
if (el.id) s += '#' + el.id;
|
|
23
|
+
if (el.className && typeof el.className === 'string') {
|
|
24
|
+
s += '.' + el.className.trim().split(/\\s+/).join('.');
|
|
25
|
+
}
|
|
26
|
+
return s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeRect(rect) {
|
|
30
|
+
return { width: rect.width, height: rect.height, left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isIgnored(el, extra) {
|
|
34
|
+
var sels = IGNORE.concat(extra || []);
|
|
35
|
+
for (var i = 0; i < sels.length; i++) {
|
|
36
|
+
try { if (el.matches(sels[i])) return true; } catch(e) {}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isHidden(el) {
|
|
42
|
+
var s = getComputedStyle(el);
|
|
43
|
+
return s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isOffScreen(rect) {
|
|
47
|
+
return rect.right < 0 || rect.bottom < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeElement(el, rect) {
|
|
51
|
+
return {
|
|
52
|
+
selector: makeSelector(el),
|
|
53
|
+
tagName: el.tagName,
|
|
54
|
+
className: typeof el.className === 'string' ? el.className : '',
|
|
55
|
+
id: el.id || null,
|
|
56
|
+
textContent: (el.textContent || '').slice(0, 50).trim() || null,
|
|
57
|
+
rect: makeRect(rect || el.getBoundingClientRect()),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// === Check: Overflow ===
|
|
62
|
+
function checkOverflow() {
|
|
63
|
+
var issues = [];
|
|
64
|
+
var vw = window.innerWidth;
|
|
65
|
+
var docW = document.documentElement.scrollWidth;
|
|
66
|
+
if (docW <= vw) return { check: 'overflow', passed: 1, issues: issues };
|
|
67
|
+
|
|
68
|
+
var all = document.querySelectorAll('*');
|
|
69
|
+
var culprits = [];
|
|
70
|
+
for (var i = 0; i < all.length; i++) {
|
|
71
|
+
var el = all[i];
|
|
72
|
+
if (isIgnored(el)) continue;
|
|
73
|
+
var rect = el.getBoundingClientRect();
|
|
74
|
+
if (rect.right > vw + 1) {
|
|
75
|
+
culprits.push({ el: el, rect: rect, overflow: rect.right - vw });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
culprits.sort(function(a, b) { return b.overflow - a.overflow; });
|
|
79
|
+
culprits = culprits.slice(0, 10);
|
|
80
|
+
|
|
81
|
+
if (culprits.length > 0) {
|
|
82
|
+
var main = culprits[0];
|
|
83
|
+
issues.push({
|
|
84
|
+
check: 'overflow',
|
|
85
|
+
severity: 'error',
|
|
86
|
+
message: 'Horizontal overflow detected (page scrolls ' + (docW - vw) + 'px beyond viewport)',
|
|
87
|
+
element: makeElement(main.el, main.rect),
|
|
88
|
+
details: {
|
|
89
|
+
documentScrollWidth: docW,
|
|
90
|
+
viewportWidth: vw,
|
|
91
|
+
overflowAmount: main.overflow,
|
|
92
|
+
otherCulprits: culprits.slice(1).map(function(c) { return makeSelector(c.el); }),
|
|
93
|
+
},
|
|
94
|
+
fixHint: 'Add overflow-x: hidden to a container, or check for elements with fixed widths, 100vw, or negative margins',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { check: 'overflow', passed: issues.length === 0 ? 1 : 0, issues: issues };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// === Check: Clickability ===
|
|
101
|
+
function checkClickability() {
|
|
102
|
+
var issues = [];
|
|
103
|
+
var EDGE_INSET = 2;
|
|
104
|
+
var selectorStr = INTERACTIVE.join(', ');
|
|
105
|
+
var elements = document.querySelectorAll(selectorStr);
|
|
106
|
+
var total = 0;
|
|
107
|
+
|
|
108
|
+
function isCovered(el, x, y) {
|
|
109
|
+
if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) return null;
|
|
110
|
+
var top = document.elementFromPoint(x, y);
|
|
111
|
+
if (top && top !== el && !el.contains(top) && !top.contains(el)) return top;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (var i = 0; i < elements.length; i++) {
|
|
116
|
+
var el = elements[i];
|
|
117
|
+
if (isIgnored(el)) continue;
|
|
118
|
+
if (isHidden(el)) continue;
|
|
119
|
+
var rect = el.getBoundingClientRect();
|
|
120
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
121
|
+
if (isOffScreen(rect)) continue;
|
|
122
|
+
total++;
|
|
123
|
+
|
|
124
|
+
var points = [
|
|
125
|
+
{ name: 'center', x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
126
|
+
{ name: 'top-left', x: rect.left + EDGE_INSET, y: rect.top + EDGE_INSET },
|
|
127
|
+
{ name: 'top-right', x: rect.right - EDGE_INSET, y: rect.top + EDGE_INSET },
|
|
128
|
+
{ name: 'bottom-left', x: rect.left + EDGE_INSET, y: rect.bottom - EDGE_INSET },
|
|
129
|
+
{ name: 'bottom-right', x: rect.right - EDGE_INSET, y: rect.bottom - EDGE_INSET },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
var coveredPoints = [];
|
|
133
|
+
var firstCoverer = null;
|
|
134
|
+
for (var j = 0; j < points.length; j++) {
|
|
135
|
+
var coverer = isCovered(el, points[j].x, points[j].y);
|
|
136
|
+
if (coverer) {
|
|
137
|
+
coveredPoints.push(points[j].name);
|
|
138
|
+
if (!firstCoverer) firstCoverer = coverer;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (coveredPoints.length > 0 && firstCoverer) {
|
|
143
|
+
var allCovered = coveredPoints.length === points.length;
|
|
144
|
+
var desc = allCovered ? 'fully covered' : 'partially covered (' + coveredPoints.join(', ') + ')';
|
|
145
|
+
issues.push({
|
|
146
|
+
check: 'clickability',
|
|
147
|
+
severity: 'error',
|
|
148
|
+
message: 'Interactive element is ' + desc + ' by another element',
|
|
149
|
+
element: makeElement(el, rect),
|
|
150
|
+
details: {
|
|
151
|
+
coveredBy: makeSelector(firstCoverer),
|
|
152
|
+
coveredPoints: coveredPoints,
|
|
153
|
+
totalPoints: points.length,
|
|
154
|
+
},
|
|
155
|
+
fixHint: 'Check z-index of ' + makeSelector(el) + ' and ' + makeSelector(firstCoverer) + ', or remove/reposition the covering element',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { check: 'clickability', passed: Math.max(0, total - issues.length), issues: issues };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// === Check: Touch Targets ===
|
|
163
|
+
function checkTouchTargets() {
|
|
164
|
+
var issues = [];
|
|
165
|
+
var MIN_W = 44, MIN_H = 44, MIN_GAP = 8, MAX_PAIRS = 20;
|
|
166
|
+
var sels = ['button','a','input:not([type="hidden"])','select','textarea','[role="button"]','[role="link"]','[role="checkbox"]','[role="radio"]','[tabindex="0"]'];
|
|
167
|
+
var elements = document.querySelectorAll(sels.join(', '));
|
|
168
|
+
var visible = [];
|
|
169
|
+
var total = 0;
|
|
170
|
+
|
|
171
|
+
for (var i = 0; i < elements.length; i++) {
|
|
172
|
+
var el = elements[i];
|
|
173
|
+
if (isIgnored(el)) continue;
|
|
174
|
+
if (isHidden(el)) continue;
|
|
175
|
+
var rect = el.getBoundingClientRect();
|
|
176
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
177
|
+
if (isOffScreen(rect)) continue;
|
|
178
|
+
visible.push({ el: el, rect: rect });
|
|
179
|
+
total++;
|
|
180
|
+
|
|
181
|
+
if (rect.width < MIN_W || rect.height < MIN_H) {
|
|
182
|
+
issues.push({
|
|
183
|
+
check: 'touch-targets',
|
|
184
|
+
severity: 'warning',
|
|
185
|
+
message: 'Touch target too small: ' + Math.round(rect.width) + 'x' + Math.round(rect.height) + 'px (min: ' + MIN_W + 'x' + MIN_H + 'px)',
|
|
186
|
+
element: makeElement(el, rect),
|
|
187
|
+
details: { actualWidth: rect.width, actualHeight: rect.height, minWidth: MIN_W, minHeight: MIN_H },
|
|
188
|
+
fixHint: 'Add min-width: ' + MIN_W + 'px and min-height: ' + MIN_H + 'px, or increase padding',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Spacing check
|
|
194
|
+
var pairCount = 0;
|
|
195
|
+
for (var a = 0; a < visible.length && pairCount < MAX_PAIRS; a++) {
|
|
196
|
+
for (var b = a + 1; b < visible.length && pairCount < MAX_PAIRS; b++) {
|
|
197
|
+
var va = visible[a], vb = visible[b];
|
|
198
|
+
if (va.el.contains(vb.el) || vb.el.contains(va.el)) continue;
|
|
199
|
+
|
|
200
|
+
var hGap = Math.max(0, Math.max(vb.rect.left - va.rect.right, va.rect.left - vb.rect.right));
|
|
201
|
+
var vGap = Math.max(0, Math.max(vb.rect.top - va.rect.bottom, va.rect.top - vb.rect.bottom));
|
|
202
|
+
var oH = va.rect.right > vb.rect.left && vb.rect.right > va.rect.left;
|
|
203
|
+
var oV = va.rect.bottom > vb.rect.top && vb.rect.bottom > va.rect.top;
|
|
204
|
+
|
|
205
|
+
var gap;
|
|
206
|
+
if (oH && oV) gap = 0;
|
|
207
|
+
else if (oH) gap = vGap;
|
|
208
|
+
else if (oV) gap = hGap;
|
|
209
|
+
else gap = Math.sqrt(hGap * hGap + vGap * vGap);
|
|
210
|
+
|
|
211
|
+
if (gap < MIN_GAP) {
|
|
212
|
+
pairCount++;
|
|
213
|
+
var selA = makeSelector(va.el), selB = makeSelector(vb.el);
|
|
214
|
+
issues.push({
|
|
215
|
+
check: 'touch-targets',
|
|
216
|
+
severity: 'warning',
|
|
217
|
+
message: 'Touch targets too close: ' + Math.round(gap) + 'px gap between ' + selA + ' and ' + selB + ' (min: ' + MIN_GAP + 'px)',
|
|
218
|
+
element: makeElement(va.el, va.rect),
|
|
219
|
+
details: { gap: Math.round(gap), minGap: MIN_GAP, nearElement: selB },
|
|
220
|
+
fixHint: 'Add at least ' + MIN_GAP + 'px margin between ' + selA + ' and ' + selB,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { check: 'touch-targets', passed: Math.max(0, total - issues.length), issues: issues };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// === Check: Visibility ===
|
|
229
|
+
function checkVisibility() {
|
|
230
|
+
var issues = [];
|
|
231
|
+
var sels = ['button','a[href]','input:not([type="hidden"])','select','textarea','[role="button"]'];
|
|
232
|
+
var ignoreExtra = ['[aria-hidden="true"]','.sr-only','.visually-hidden','[hidden]'];
|
|
233
|
+
var elements = document.querySelectorAll(sels.join(', '));
|
|
234
|
+
var total = 0;
|
|
235
|
+
|
|
236
|
+
for (var i = 0; i < elements.length; i++) {
|
|
237
|
+
var el = elements[i];
|
|
238
|
+
if (isIgnored(el, ignoreExtra)) continue;
|
|
239
|
+
total++;
|
|
240
|
+
|
|
241
|
+
var style = getComputedStyle(el);
|
|
242
|
+
var rect = el.getBoundingClientRect();
|
|
243
|
+
var reason = null;
|
|
244
|
+
|
|
245
|
+
if (style.display === 'none') reason = 'display: none';
|
|
246
|
+
else if (style.visibility === 'hidden') reason = 'visibility: hidden';
|
|
247
|
+
else if (style.opacity === '0') reason = 'opacity: 0';
|
|
248
|
+
else if (rect.width === 0 && rect.height === 0) reason = 'zero size (0x0)';
|
|
249
|
+
else if (rect.right < 0) reason = 'off-screen left';
|
|
250
|
+
else if (rect.bottom < 0) reason = 'off-screen top';
|
|
251
|
+
else if (rect.left > window.innerWidth) reason = 'off-screen right';
|
|
252
|
+
else if (rect.top > window.innerHeight) reason = 'off-screen bottom';
|
|
253
|
+
else if (style.clipPath && style.clipPath.indexOf('inset(100%)') !== -1) reason = 'clipped (clip-path)';
|
|
254
|
+
|
|
255
|
+
if (reason) {
|
|
256
|
+
var parent = el.parentElement;
|
|
257
|
+
if (parent) {
|
|
258
|
+
var ps = getComputedStyle(parent);
|
|
259
|
+
if (ps.display === 'none' || ps.visibility === 'hidden') continue;
|
|
260
|
+
}
|
|
261
|
+
issues.push({
|
|
262
|
+
check: 'visibility',
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
message: 'Interactive element is not visible: ' + reason,
|
|
265
|
+
element: makeElement(el, rect),
|
|
266
|
+
details: { reason: reason },
|
|
267
|
+
fixHint: 'Check if this element should be visible. If intentionally hidden, add [data-rlint-ignore] or .sr-only',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { check: 'visibility', passed: Math.max(0, total - issues.length), issues: issues };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// === Check: Text Overflow ===
|
|
275
|
+
function checkTextOverflow() {
|
|
276
|
+
var issues = [];
|
|
277
|
+
var ignoreExtra = ['.truncate','[data-truncate]','.line-clamp'];
|
|
278
|
+
var elements = document.querySelectorAll('p, span, h1, h2, h3, h4, h5, h6, label, li, td, th, div, a');
|
|
279
|
+
|
|
280
|
+
for (var i = 0; i < elements.length; i++) {
|
|
281
|
+
var el = elements[i];
|
|
282
|
+
if (isIgnored(el, ignoreExtra)) continue;
|
|
283
|
+
var style = getComputedStyle(el);
|
|
284
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
285
|
+
if (!(el.textContent || '').trim()) continue;
|
|
286
|
+
|
|
287
|
+
var hOver = el.scrollWidth > el.clientWidth + 1;
|
|
288
|
+
var vOver = el.scrollHeight > el.clientHeight + 1;
|
|
289
|
+
if (!hOver && !vOver) continue;
|
|
290
|
+
|
|
291
|
+
var hasScroll = ['scroll','auto'].indexOf(style.overflow) !== -1 ||
|
|
292
|
+
['scroll','auto'].indexOf(style.overflowX) !== -1 ||
|
|
293
|
+
['scroll','auto'].indexOf(style.overflowY) !== -1;
|
|
294
|
+
if (hasScroll) continue;
|
|
295
|
+
|
|
296
|
+
if (style.textOverflow === 'ellipsis') continue;
|
|
297
|
+
if (style.webkitLineClamp) continue;
|
|
298
|
+
|
|
299
|
+
var isClipping = style.overflow === 'hidden' || style.overflowX === 'hidden' || style.overflowY === 'hidden';
|
|
300
|
+
if (isClipping || (!hasScroll && (hOver || vOver))) {
|
|
301
|
+
var rect = el.getBoundingClientRect();
|
|
302
|
+
var dir = (hOver && vOver) ? 'both directions' : hOver ? 'horizontally' : 'vertically';
|
|
303
|
+
issues.push({
|
|
304
|
+
check: 'text-overflow',
|
|
305
|
+
severity: 'warning',
|
|
306
|
+
message: 'Text overflows container ' + dir,
|
|
307
|
+
element: makeElement(el, rect),
|
|
308
|
+
details: { scrollWidth: el.scrollWidth, clientWidth: el.clientWidth, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight },
|
|
309
|
+
fixHint: 'Add overflow: auto for scrolling, text-overflow: ellipsis for truncation, or increase container size',
|
|
310
|
+
});
|
|
311
|
+
if (issues.length >= 20) break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { check: 'text-overflow', passed: 0, issues: issues };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// === Check: Viewport Meta ===
|
|
318
|
+
function checkViewportMeta() {
|
|
319
|
+
var issues = [];
|
|
320
|
+
var dummy = { selector: 'head', tagName: 'HEAD', className: '', id: null, textContent: null, rect: { width: 0, height: 0, left: 0, top: 0, right: 0, bottom: 0 } };
|
|
321
|
+
var meta = document.querySelector('meta[name="viewport"]');
|
|
322
|
+
|
|
323
|
+
if (!meta) {
|
|
324
|
+
issues.push({
|
|
325
|
+
check: 'viewport-meta',
|
|
326
|
+
severity: 'warning',
|
|
327
|
+
message: 'Missing viewport meta tag — page will not render correctly on mobile devices',
|
|
328
|
+
element: dummy,
|
|
329
|
+
details: { expected: '<meta name="viewport" content="width=device-width, initial-scale=1">' },
|
|
330
|
+
fixHint: 'Add <meta name="viewport" content="width=device-width, initial-scale=1"> to <head>',
|
|
331
|
+
});
|
|
332
|
+
} else {
|
|
333
|
+
var content = meta.getAttribute('content') || '';
|
|
334
|
+
var directives = content.split(',').map(function(d) { return d.trim().toLowerCase(); });
|
|
335
|
+
|
|
336
|
+
var hasWidth = directives.some(function(d) { return d.replace(/\\s+/g, '').indexOf('width=device-width') === 0; });
|
|
337
|
+
var hasNoScale = directives.some(function(d) { var c = d.replace(/\\s+/g, ''); return c === 'user-scalable=no' || c === 'user-scalable=0'; });
|
|
338
|
+
var hasMaxScale = directives.some(function(d) { var c = d.replace(/\\s+/g, ''); return c.indexOf('maximum-scale=') === 0 && parseFloat(c.split('=')[1]) <= 1; });
|
|
339
|
+
|
|
340
|
+
if (!hasWidth) {
|
|
341
|
+
issues.push({
|
|
342
|
+
check: 'viewport-meta',
|
|
343
|
+
severity: 'warning',
|
|
344
|
+
message: 'Viewport meta tag is missing width=device-width — mobile browsers will use default 980px width',
|
|
345
|
+
element: dummy,
|
|
346
|
+
details: { current: content, expected: 'width=device-width' },
|
|
347
|
+
fixHint: 'Add width=device-width to your viewport meta tag',
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (hasNoScale) {
|
|
351
|
+
issues.push({
|
|
352
|
+
check: 'viewport-meta',
|
|
353
|
+
severity: 'warning',
|
|
354
|
+
message: 'Viewport meta tag disables user scaling — this is an accessibility problem (WCAG 1.4.4)',
|
|
355
|
+
element: dummy,
|
|
356
|
+
details: { current: content, problem: 'user-scalable=no' },
|
|
357
|
+
fixHint: 'Remove user-scalable=no from your viewport meta tag to allow pinch-to-zoom',
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (hasMaxScale) {
|
|
361
|
+
issues.push({
|
|
362
|
+
check: 'viewport-meta',
|
|
363
|
+
severity: 'warning',
|
|
364
|
+
message: 'Viewport meta tag restricts maximum scale to 1 — this prevents pinch-to-zoom (WCAG 1.4.4)',
|
|
365
|
+
element: dummy,
|
|
366
|
+
details: { current: content, problem: 'maximum-scale=1' },
|
|
367
|
+
fixHint: 'Remove maximum-scale=1 or set it to at least 5 to allow zooming',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { check: 'viewport-meta', passed: issues.length === 0 ? 1 : 0, issues: issues };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// === Runner ===
|
|
375
|
+
function runAllChecks() {
|
|
376
|
+
var checks = [checkOverflow, checkClickability, checkTouchTargets, checkVisibility, checkTextOverflow, checkViewportMeta];
|
|
377
|
+
var allIssues = [];
|
|
378
|
+
var passed = 0;
|
|
379
|
+
|
|
380
|
+
for (var i = 0; i < checks.length; i++) {
|
|
381
|
+
try {
|
|
382
|
+
var result = checks[i]();
|
|
383
|
+
allIssues = allIssues.concat(result.issues);
|
|
384
|
+
passed += result.passed;
|
|
385
|
+
} catch(e) {
|
|
386
|
+
console.warn('[rlint] check error:', e);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
var errors = 0, warnings = 0;
|
|
391
|
+
for (var j = 0; j < allIssues.length; j++) {
|
|
392
|
+
if (allIssues[j].severity === 'error') errors++;
|
|
393
|
+
else if (allIssues[j].severity === 'warning') warnings++;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
var results = {
|
|
397
|
+
url: location.pathname + location.search,
|
|
398
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
399
|
+
timestamp: new Date().toISOString(),
|
|
400
|
+
summary: { passed: passed, errors: errors, warnings: warnings },
|
|
401
|
+
issues: allIssues,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
window.__rlint__.results = results;
|
|
405
|
+
|
|
406
|
+
fetch(ENDPOINT, {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
409
|
+
body: JSON.stringify(results),
|
|
410
|
+
}).catch(function() {});
|
|
411
|
+
|
|
412
|
+
return results;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// === Navigation detection ===
|
|
416
|
+
var debounceTimer = null;
|
|
417
|
+
function scheduleCheck() {
|
|
418
|
+
clearTimeout(debounceTimer);
|
|
419
|
+
debounceTimer = setTimeout(runAllChecks, 800);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
var origPush = history.pushState;
|
|
423
|
+
var origReplace = history.replaceState;
|
|
424
|
+
history.pushState = function() { origPush.apply(this, arguments); scheduleCheck(); };
|
|
425
|
+
history.replaceState = function() { origReplace.apply(this, arguments); scheduleCheck(); };
|
|
426
|
+
window.addEventListener('popstate', scheduleCheck);
|
|
427
|
+
|
|
428
|
+
// === Initial run ===
|
|
429
|
+
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
430
|
+
setTimeout(runAllChecks, 500);
|
|
431
|
+
} else {
|
|
432
|
+
window.addEventListener('DOMContentLoaded', function() { setTimeout(runAllChecks, 500); });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// === MutationObserver for dynamic content ===
|
|
436
|
+
setTimeout(function() {
|
|
437
|
+
var mutTimer = null;
|
|
438
|
+
var observer = new MutationObserver(function() {
|
|
439
|
+
clearTimeout(mutTimer);
|
|
440
|
+
mutTimer = setTimeout(runAllChecks, 2000);
|
|
441
|
+
});
|
|
442
|
+
if (document.body) {
|
|
443
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
444
|
+
}
|
|
445
|
+
}, 3000);
|
|
446
|
+
|
|
447
|
+
})();`;
|
|
448
|
+
}
|
|
449
|
+
//# sourceMappingURL=client-script.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-script.js","sourceRoot":"","sources":["../../src/proxy/client-script.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAsbH,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAuC,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAqB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface CachedResult {
|
|
2
|
+
url: string;
|
|
3
|
+
viewport: {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
timestamp: string;
|
|
8
|
+
summary: {
|
|
9
|
+
passed: number;
|
|
10
|
+
errors: number;
|
|
11
|
+
warnings: number;
|
|
12
|
+
};
|
|
13
|
+
issues: Array<{
|
|
14
|
+
check: string;
|
|
15
|
+
severity: 'error' | 'warning' | 'info';
|
|
16
|
+
message: string;
|
|
17
|
+
element: {
|
|
18
|
+
selector: string;
|
|
19
|
+
tagName: string;
|
|
20
|
+
className: string;
|
|
21
|
+
id: string | null;
|
|
22
|
+
textContent: string | null;
|
|
23
|
+
rect: {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
left: number;
|
|
27
|
+
top: number;
|
|
28
|
+
right: number;
|
|
29
|
+
bottom: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
details: Record<string, unknown>;
|
|
33
|
+
fixHint?: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare class ResultCache {
|
|
37
|
+
private cache;
|
|
38
|
+
set(url: string, result: CachedResult): void;
|
|
39
|
+
get(url: string): CachedResult | undefined;
|
|
40
|
+
getAll(): CachedResult[];
|
|
41
|
+
has(url: string): boolean;
|
|
42
|
+
clear(): void;
|
|
43
|
+
get size(): number;
|
|
44
|
+
private normalize;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=result-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result-cache.d.ts","sourceRoot":"","sources":["../../src/proxy/result-cache.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,MAAM,EAAE,KAAK,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;QACvC,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE;YACP,QAAQ,EAAE,MAAM,CAAC;YACjB,OAAO,EAAE,MAAM,CAAC;YAChB,SAAS,EAAE,MAAM,CAAC;YAClB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;YAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;YAC3B,IAAI,EAAE;gBAAE,KAAK,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,GAAG,EAAE,MAAM,CAAC;gBAAC,KAAK,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,CAAA;aAAE,CAAC;SACnG,CAAC;QACF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;CACJ;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAmC;IAEhD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI;IAK5C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI1C,MAAM,IAAI,YAAY,EAAE;IAIxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,CAAC,SAAS;CAIlB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class ResultCache {
|
|
2
|
+
cache = new Map();
|
|
3
|
+
set(url, result) {
|
|
4
|
+
const key = this.normalize(url);
|
|
5
|
+
this.cache.set(key, result);
|
|
6
|
+
}
|
|
7
|
+
get(url) {
|
|
8
|
+
return this.cache.get(this.normalize(url));
|
|
9
|
+
}
|
|
10
|
+
getAll() {
|
|
11
|
+
return Array.from(this.cache.values());
|
|
12
|
+
}
|
|
13
|
+
has(url) {
|
|
14
|
+
return this.cache.has(this.normalize(url));
|
|
15
|
+
}
|
|
16
|
+
clear() {
|
|
17
|
+
this.cache.clear();
|
|
18
|
+
}
|
|
19
|
+
get size() {
|
|
20
|
+
return this.cache.size;
|
|
21
|
+
}
|
|
22
|
+
normalize(url) {
|
|
23
|
+
// Strip trailing slash and hash fragment
|
|
24
|
+
return url.replace(/\/$/, '').replace(/#.*$/, '');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=result-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result-cache.js","sourceRoot":"","sources":["../../src/proxy/result-cache.ts"],"names":[],"mappings":"AAsBA,MAAM,OAAO,WAAW;IACd,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAC;IAEhD,GAAG,CAAC,GAAW,EAAE,MAAoB;QACnC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC9B,CAAC;IAED,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM;QACJ,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAEO,SAAS,CAAC,GAAW;QAC3B,yCAAyC;QACzC,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpD,CAAC;CACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CachedResult } from './result-cache.js';
|
|
2
|
+
export interface ProxyOptions {
|
|
3
|
+
target: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
onResults?: (result: CachedResult) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface ProxyServer {
|
|
8
|
+
start(): Promise<void>;
|
|
9
|
+
stop(): Promise<void>;
|
|
10
|
+
getResults(): CachedResult[];
|
|
11
|
+
getResult(url: string): CachedResult | undefined;
|
|
12
|
+
clearResults(): void;
|
|
13
|
+
readonly port: number;
|
|
14
|
+
readonly target: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function createProxyServer(options: ProxyOptions): ProxyServer;
|
|
17
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEnE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,UAAU,IAAI,YAAY,EAAE,CAAC;IAC7B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IACjD,YAAY,IAAI,IAAI,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,WAAW,CAqNpE"}
|