Add temporary script to detect usage of Bootstrap v5 deprecated classes#42487
Conversation
ef240c6 to
fedf4e4
Compare
fedf4e4 to
03d0422
Compare
|
Added an option to the script to be able to run `.log` output example
╔══════════════════════════════════════════════════════════════════════╗
║ Bootstrap v5 → v6 Deprecated Classes Report ║
╚══════════════════════════════════════════════════════════════════════╝
|
03d0422 to
743713f
Compare
743713f to
21bf73e
Compare
|
╔══════════════════════════════════════════════════════════════════════╗ Generated : 2026-07-23T17:32:08.603Z SUMMARY USED DEPRECATED CLASSES [only classes still found in this codebase]
|
|
@julien-deramond - that's helpful - thanks! I have a PR open which addresses some of them in examples. #42455 |
21bf73e to
4bd5778
Compare
4bd5778 to
54b9131
Compare
54b9131 to
269dae8
Compare
7c10364 to
70bddac
Compare
6face5f to
38af0d4
Compare
38af0d4 to
d2d6786
Compare
d2d6786 to
c7ec4d0
Compare
Greptile SummaryThis PR adds a temporary developer script (
Confidence Score: 4/5Safe to merge as a temporary dev utility; the script produces correct output but runs slower than necessary due to avoidable work inside the scan loop. The scan loop reconstructs each class regex and performs a linear indexOf lookup on every (file, class) pair instead of pre-compiling patterns once — with ~1428 classes and ~435 files this is measurable overhead on every run. There is also no fetch timeout, so a GitHub connectivity issue will hang the process indefinitely. build/check-deprecated-v5-classes.mjs — specifically the scan loop (lines 144-180) and the unconstrained fetch call (line 64). Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A([npm run check-deprecated-v5-classes]) --> B[npm run css\nCompile v6 CSS]
B --> C[Read dist/css/bootstrap.css\nExtract v6 class set]
C --> D[fetch GitHub main branch\ndist/css/bootstrap.css]
D --> E[Extract v5 class set]
E --> F[Compute v5-only classes\ndeprecated set]
F --> G[Walk codebase files\nby extension]
G --> H{For each file ×\neach deprecated class}
H --> I[Construct RegExp\nper class per file]
I --> J[Scan lines for match]
J --> K{More files\nor classes?}
K -->|yes| H
K -->|no| L[Write timestamped\n.log to repo root]
L --> M{findings.size > 0?}
M -->|yes| N([exit 1 — deprecated classes found])
M -->|no| O([exit 0 — clean])
Reviews (1): Last reviewed commit: "Exclude skills and AGENTS.md" | Re-trigger Greptile |
| const escapedClasses = deprecatedClasses.map(c => | ||
| c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | ||
| ) | ||
|
|
||
| // findings: Map<className, Array<{file, lineNumber, lineText}>> | ||
| const findings = new Map() | ||
|
|
||
| let fileCount = 0 | ||
| for (const filePath of walkDir(ROOT)) { | ||
| fileCount++ | ||
| const relPath = relative(ROOT, filePath) | ||
| let content | ||
| try { | ||
| content = readFileSync(filePath, 'utf8') | ||
| } catch { | ||
| continue | ||
| } | ||
|
|
||
| const lines = content.split('\n') | ||
|
|
||
| for (const cls of deprecatedClasses) { | ||
| // Non-global regex – no lastIndex drift | ||
| const pattern = new RegExp( | ||
| `(?<![a-zA-Z0-9_-])${escapedClasses[deprecatedClasses.indexOf(cls)]}(?![a-zA-Z0-9_-])` | ||
| ) | ||
| for (const [i, line] of lines.entries()) { | ||
| if (pattern.test(line)) { | ||
| if (!findings.has(cls)) {findings.set(cls, [])} | ||
| findings.get(cls).push({ | ||
| file: relPath, | ||
| lineNumber: i + 1, | ||
| lineText: line.trim().slice(0, 160) | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The
RegExp for each class is compiled fresh once per file (1428 classes × 435 files ≈ 621 K regex compilations), and deprecatedClasses.indexOf(cls) does a linear O(n) scan of the ~1428-item array on every single (file, class) pair — adding roughly 1428² ≈ 2 M string comparisons per file. Pre-compiling the patterns once before the file loop and using entries() to zip class + escape index eliminates both costs entirely.
| const escapedClasses = deprecatedClasses.map(c => | |
| c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | |
| ) | |
| // findings: Map<className, Array<{file, lineNumber, lineText}>> | |
| const findings = new Map() | |
| let fileCount = 0 | |
| for (const filePath of walkDir(ROOT)) { | |
| fileCount++ | |
| const relPath = relative(ROOT, filePath) | |
| let content | |
| try { | |
| content = readFileSync(filePath, 'utf8') | |
| } catch { | |
| continue | |
| } | |
| const lines = content.split('\n') | |
| for (const cls of deprecatedClasses) { | |
| // Non-global regex – no lastIndex drift | |
| const pattern = new RegExp( | |
| `(?<![a-zA-Z0-9_-])${escapedClasses[deprecatedClasses.indexOf(cls)]}(?![a-zA-Z0-9_-])` | |
| ) | |
| for (const [i, line] of lines.entries()) { | |
| if (pattern.test(line)) { | |
| if (!findings.has(cls)) {findings.set(cls, [])} | |
| findings.get(cls).push({ | |
| file: relPath, | |
| lineNumber: i + 1, | |
| lineText: line.trim().slice(0, 160) | |
| }) | |
| } | |
| } | |
| } | |
| } | |
| const escapedClasses = deprecatedClasses.map(c => | |
| c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | |
| ) | |
| // Pre-compile all patterns once – avoids repeated regex construction and O(n) indexOf per file | |
| const patterns = deprecatedClasses.map((_, i) => | |
| new RegExp(`(?<![a-zA-Z0-9_-])${escapedClasses[i]}(?![a-zA-Z0-9_-])`) | |
| ) | |
| // findings: Map<className, Array<{file, lineNumber, lineText}>> | |
| const findings = new Map() | |
| let fileCount = 0 | |
| for (const filePath of walkDir(ROOT)) { | |
| fileCount++ | |
| const relPath = relative(ROOT, filePath) | |
| let content | |
| try { | |
| content = readFileSync(filePath, 'utf8') | |
| } catch { | |
| continue | |
| } | |
| const lines = content.split('\n') | |
| for (const [clsIndex, cls] of deprecatedClasses.entries()) { | |
| const pattern = patterns[clsIndex] | |
| for (const [i, line] of lines.entries()) { | |
| if (pattern.test(line)) { | |
| if (!findings.has(cls)) {findings.set(cls, [])} | |
| findings.get(cls).push({ | |
| file: relPath, | |
| lineNumber: i + 1, | |
| lineText: line.trim().slice(0, 160) | |
| }) | |
| } | |
| } | |
| } | |
| } |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const v5Response = await fetch(V5_URL) | ||
| if (!v5Response.ok) { | ||
| throw new Error(`Failed to fetch v5 CSS: ${v5Response.status} ${v5Response.statusText}`) | ||
| } |
There was a problem hiding this comment.
The
fetch call has no timeout, so if GitHub's raw content endpoint is slow or unresponsive, the script will hang indefinitely. AbortSignal.timeout() is available natively in Node 17.3+ and keeps the failure mode predictable.
| const v5Response = await fetch(V5_URL) | |
| if (!v5Response.ok) { | |
| throw new Error(`Failed to fetch v5 CSS: ${v5Response.status} ${v5Response.statusText}`) | |
| } | |
| const v5Response = await fetch(V5_URL, { signal: AbortSignal.timeout(30_000) }) | |
| if (!v5Response.ok) { | |
| throw new Error(`Failed to fetch v5 CSS: ${v5Response.status} ${v5Response.statusText}`) | |
| } |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) | ||
| const logPath = join(ROOT, `deprecated-classes-${timestamp}.log`) |
There was a problem hiding this comment.
Log files accumulate in the repo root — each run writes a new
deprecated-classes-<timestamp>.log file directly at ROOT. If this pattern isn't covered by .gitignore (e.g. *.log or deprecated-classes-*.log), repeated runs will litter the working tree with untracked files. Consider writing to a dedicated folder like build/reports/ or ensuring the glob is ignored.
Description
This PR adds a temporary vibe coded script to detect usages of Bootstrap v5 classes that no longer exist in v6 across our codebase, in a similar spirit to the approach used by the Bootstrap Deprecated Classes extension: https://github.com/julien-deramond/bootstrap-deprecated-classes-extension.
It's only been lightly tested so far (and only on my macOS environment), but the results I've spot-checked all appear to be legitimate v5 classes that should no longer be used in v6.
I'm not sure whether it makes sense to merge this, since its purpose is mainly to help with the migration work on this branch. If not, @mdo and/or @coliff may still find it useful.
Example output from a run today:
The corresponding report is generated at the repository root:
deprecated-classes-2026-06-08T17-50-24.log
Warning
Some classes are considered found in v6 codebase because of their simple names. For now, I've kept them in the results. I'm not sure it's easy to better spot what's a class or not in the docs.