Setting the file. One moment.
Live Browser Ignores · Impeccable · pbakaus/impeccable · Skills Docs
ContentsBack to the top of the page Next
Script Live Browser Session
scripts/ live-browser-ignores.js
JavaScript · 242 lines · 10 KB
15 * (isIgnoredFindingValue returns false for it), so neither does this.
16 * 3. Remaining `ignoreValues` entries match on the finding's own value;
17 * those are forwarded as `disabledValues` for the detector bundle to
18 * apply where the findings are assembled.
19 * 4. `ignoreFiles` globs that name the page waive it wholesale: the
20 * resolver reports `skipScan: true` and the detector answers the scan
21 * with zero findings, mirroring shouldIgnoreDetectionFile in the CLI
22 * and the edit hook's own ignoreFiles gate.
23 *
24 * `pageFiles`, when the server could resolve it, lists the real project
25 * files the inject config serves. A URL that suffix-matches exactly one of
26 * them takes that file as its only project identity; an ambiguous or absent
27 * match falls back to the served-root common ancestor below.
28 *
29 * Known gap, unchanged from PR #645: framework apps inject into source files
30 * (src/routes/about/+page.svelte) while scans see route URLs (/about), so
31 * entries scoped to source or asset paths never match a page candidate and
32 * are dropped. That shows the finding, which is the conservative direction.
33 *
34 * Kept separate from live-browser.js so the glob and page-scope logic can be
35 * unit tested in Node (tests/live-browser-ignores.test.mjs) without the full
36 * overlay UI bundle.
37 */
38 ( function ( root ) {
39 'use strict' ;
40 if ( ! root) return ;
41
42 // Keep in step with normalizeIgnoreRule / normalizeIgnoreValue in
43 // cli/lib/impeccable-config.mjs.
44 function normalizeIgnoreRule ( rule ) {
45 return String (rule || '' ). trim (). toLowerCase ();
46 }
47
48 function normalizeIgnoreValue ( value ) {
49 return String (value || '' )
50 . trim ()
51 . replace ( / ^ ["'] | ["'] $ / g , '' )
52 . replace ( / \+ / g , ' ' )
53 . replace ( / \s + / g , ' ' )
54 . toLowerCase ();
55 }
56
57 // Glob -> RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation.
58 // Keep in step with globToRegex in cli/lib/impeccable-config.mjs.
59 function globToRegex ( glob ) {
60 let re = '^' ;
61 let i = 0 ;
62 while (i < glob. length ) {
63 const c = glob[i];
64 if (c === '*' ) {
65 if (glob[i + 1 ] === '*' ) {
66 re += '.*' ;
67 i += 2 ;
68 if (glob[i] === '/' ) i += 1 ;
69 } else {
70 re += '[^/]*' ;
71 i += 1 ;
72 }
73 } else if (c === '?' ) {
74 re += '[^/]' ;
75 i += 1 ;
76 } else if (c === '{' ) {
77 const end = glob. indexOf ( '}' , i);
78 if (end === - 1 ) { re += ' \\ {' ; i += 1 ; continue ; }
79 const parts = glob. slice (i + 1 , end). split ( ',' ). map (( p ) => p. replace ( / [.+^$()|[ \]\\ ] / g , ' \\ $&' ));
80 re += `(?:${ parts . join ( '|' ) })` ;
81 i = end + 1 ;
82 } else if ( / [.+^$()|[ \]\\ ] / . test (c)) {
83 re += ` \\ ${ c }` ;
84 i += 1 ;
85 } else {
86 re += c;
87 i += 1 ;
88 }
89 }
90 re += '$' ;
91 return new RegExp (re);
92 }
93
94 // The project-relative paths this page could be known as. Ignore globs are
95 // project-relative (prototype/foo.html) and the URL is site-relative
96 // (/foo.html), because a static server's root usually sits inside the
97 // project; `roots` carries that prefix. The server reads it from the inject
98 // config's own `files` globs, which already state where the served pages
99 // are. Do not derive it from the ignore globs: a single entry scoped to
100 // prototype/library/** would then lend prototype/library/ as a candidate
101 // prefix to every page, and that rule would suppress site-wide.
102 //
103 // Each prefixed path also contributes its slash suffixes, mirroring
104 // findingMatchesScopedIgnoreFile in cli/lib/impeccable-config.mjs (which
105 // matches globs against every path suffix of the finding's file).
106 //
107 // One live session is served by one server, so a single document root must
108 // sit at or above every configured page. The only prefix that can safely
109 // be asserted is therefore the deepest common ancestor of the glob roots.
110 // Treating each glob's own prefix as an identity goes wrong in both
111 // directions: disjoint roots (src/ and public/) invent simultaneous
112 // identities for one URL, so a waiver scoped to src/foo.html hides a
113 // finding on a page served from public/foo.html; nested roots (prototype/
114 // and prototype/library/, from globs at two depths in one tree) are not
115 // alternatives at all, and demanding a waiver match under both stops
116 // prototype/index.html from applying anywhere. When the globs share no
117 // common root, no prefix is asserted and only the URL path itself matches.
118 function pageCandidates ( pathname , roots , pageFiles ) {
119 let pagePath = String (pathname || '' );
120 try {
121 pagePath = decodeURIComponent (pagePath);
122 } catch {
123 // Malformed percent-escape: match on the raw path rather than throwing.
124 }
125 pagePath = pagePath. replace ( / ^ \/ + / , '' );
126 // A directory URL serves that directory's index, and the ignore globs
127 // name files. Without this, /news/ never matches prototype/news/index.html.
128 if (pagePath === '' || pagePath. endsWith ( '/' )) pagePath += 'index.html' ;
129
130 const candidates = new Set ();
131 const addSuffixes = ( fullPath ) => {
132 const parts = fullPath. split ( '/' ). filter (Boolean);
133 for ( let i = 0 ; i < parts. length ; i ++ ) {
134 candidates. add (parts. slice (i). join ( '/' ));
135 }
136 };
137 addSuffixes (pagePath);
138
139 // The served page list names the real files the inject config serves.
140 // A URL that suffix-matches exactly one of them has an unambiguous
141 // project identity; assert that identity and stop guessing from roots
142 // (PR #645 review: with src/ and public/ both served, /foo.html must not
143 // borrow src/foo.html's waivers while actually serving public/foo.html).
144 // Zero matches or several fall through to the common-ancestor fallback:
145 // ambiguity resolves toward showing the finding.
146 const knownPages = [];
147 for ( const entry of Array. isArray (pageFiles) ? pageFiles : []) {
148 if ( typeof entry !== 'string' || ! entry) continue ;
149 if (entry === pagePath || entry. endsWith ( '/' + pagePath)) knownPages. push (entry);
150 }
151 if (knownPages. length === 1 ) {
152 addSuffixes (knownPages[ 0 ]);
153 return [ ... candidates];
154 }
155
156 const prefixes = [];
157 for ( const entry of Array. isArray (roots) ? roots : []) {
158 if ( typeof entry !== 'string' ) continue ;
159 prefixes. push (entry. split ( '/' ). filter (Boolean));
160 }
161 let common = prefixes. length > 0 ? prefixes[ 0 ] : [];
162 for ( const segments of prefixes. slice ( 1 )) {
163 let i = 0 ;
164 while (i < common. length && i < segments. length && common[i] === segments[i]) i += 1 ;
165 common = common. slice ( 0 , i);
166 }
167
168 if (common. length > 0 ) addSuffixes (common. join ( '/' ) + '/' + pagePath);
169 return [ ... candidates];
170 }
171
172 function matchesScope ( globs , candidates ) {
173 return globs. some (( glob ) => {
174 let re;
175 try {
176 re = globToRegex ( String (glob));
177 } catch {
178 // Malformed glob: skip it, as matchesAnyGlob does in the CLI.
179 return false ;
180 }
181 return candidates. some (( candidate ) => re. test (candidate));
182 });
183 }
184
185 /**
186 * Resolve the serialized project ignores for one page.
187 *
188 * @param {object} options
189 * @param {object} options.ignores window.__IMPECCABLE_PROJECT_IGNORES__,
190 * in whatever state it arrived: absent, null, or hand-edited into the
191 * wrong shape. Every read tolerates that and degrades to no filtering.
192 * @param {string} options.pathname location.pathname of the scanned page.
193 * @returns {{ disabledRules: string[], disabledValues: Array<{rule: string, value: string}>, skipScan: boolean }}
194 */
195 function resolveDetectIgnores ({ ignores , pathname } = {}) {
196 const config = ignores && typeof ignores === 'object' ? ignores : {};
197 const asArray = ( value ) => (Array. isArray (value) ? value : []);
198 const candidates = pageCandidates (pathname, config.roots, config.pageFiles);
199
200 // detector.ignoreFiles waives whole files. When any glob names this
201 // page, the scan itself is skipped; rule and value lists are returned
202 // empty because nothing will run.
203 const ignoreFileGlobs = asArray (config.ignoreFiles)
204 . filter (( glob ) => typeof glob === 'string' && glob. trim ());
205 if (ignoreFileGlobs. length > 0 && matchesScope (ignoreFileGlobs, candidates)) {
206 return { disabledRules: [], disabledValues: [], skipScan: true };
207 }
208
209 const disabledRules = new Set (
210 asArray (config.ignoreRules)
211 . filter (( rule ) => typeof rule === 'string' )
212 . map (normalizeIgnoreRule)
213 . filter (Boolean),
214 );
215 const disabledValues = [];
216
217 for ( const entry of asArray (config.ignoreValues)) {
218 if ( ! entry || typeof entry !== 'object' ) continue ;
219 const rule = normalizeIgnoreRule (entry.rule);
220 const value = normalizeIgnoreValue (entry.value);
221 if ( ! rule || ! value) continue ;
222 const files = [
223 ... ( typeof entry.file === 'string' && entry.file. trim () ? [entry.file. trim ()] : []),
224 ... asArray (entry.files). filter (( glob ) => typeof glob === 'string' && glob. trim ()),
225 ];
226 if (value === '*' ) {
227 // Wildcards suppress their rule only inside the files they name.
228 if (files. length > 0 && matchesScope (files, candidates)) disabledRules. add (rule);
229 continue ;
230 }
231 if (files. length > 0 && ! matchesScope (files, candidates)) continue ;
232 disabledValues. push ({ rule, value });
233 }
234
235 return { disabledRules: [ ... disabledRules], disabledValues, skipScan: false };
236 }
237
238 root.__IMPECCABLE_LIVE_IGNORES__ = {
239 version: 1 ,
240 resolveDetectIgnores,
241 };
242 })( typeof window !== 'undefined' ? window : globalThis);