From 725da0990b09c7bd5d40e6cf8b13b0d8a0e4beb3 Mon Sep 17 00:00:00 2001 From: NgoQuocViet2001 Date: Mon, 24 Aug 2026 15:16:52 +0700 Subject: [PATCH] fix(utils): stop generateCodeFrame hanging on a CRLF source The inner loop had no upper bound on j: for (let j = i - range; j <= i + range || end > count; j++) { if (j < 0 || j >= lines.length) continue Once j runs past the last line the continue skips the count update at the bottom of the body, so count freezes while end stays larger, and the loop spins forever at 100% CPU. It only shows on CRLF input. count advances by one per line break, but end is clamped to source.length, which counts two characters per CRLF -- so on a CRLF source end can exceed anything count reaches. On LF the two always meet and the loop exits. Bound j by lines.length in the second disjunct. --- .../__tests__/__snapshots__/utils.spec.ts.snap | 18 ++++++++++++++++++ packages/vite/src/node/__tests__/utils.spec.ts | 5 +++++ packages/vite/src/node/utils.ts | 12 +++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/__tests__/__snapshots__/utils.spec.ts.snap b/packages/vite/src/node/__tests__/__snapshots__/utils.spec.ts.snap index ccf43c253a01b3..6a1746e538dc48 100644 --- a/packages/vite/src/node/__tests__/__snapshots__/utils.spec.ts.snap +++ b/packages/vite/src/node/__tests__/__snapshots__/utils.spec.ts.snap @@ -52,6 +52,24 @@ exports[`generateCodeFrames > end 6`] = ` " `; +exports[`generateCodeFrames > end with CRLF 1`] = ` +" +1 | import foo from './foo' + | ^^^^^^^^^^^^^^^^^^^^^^^ +2 | foo() + | ^^^^^ +" +`; + +exports[`generateCodeFrames > end with CRLF 2`] = ` +" +1 | import foo from './foo' + | ^^^^^^^^^^^^^^^^^^^^^^^ +2 | foo() + | ^^^^^ +" +`; + exports[`generateCodeFrames > invalid start > end 1`] = ` " 1 | import foo from './foo' diff --git a/packages/vite/src/node/__tests__/utils.spec.ts b/packages/vite/src/node/__tests__/utils.spec.ts index 7974ab754301da..b8d93bd65bfb62 100644 --- a/packages/vite/src/node/__tests__/utils.spec.ts +++ b/packages/vite/src/node/__tests__/utils.spec.ts @@ -361,6 +361,11 @@ foo() expectSnapshot(generateCodeFrame(sourceCrLf, { line: 2, column: 0 })) }) + test('end with CRLF', () => { + expectSnapshot(generateCodeFrame(sourceCrLf, 0, sourceCrLf.length)) + expectSnapshot(generateCodeFrame(sourceCrLf, 0, sourceCrLf.length + 100)) + }) + test('end', () => { expectSnapshot(generateCodeFrame(source, 0, 0)) expectSnapshot(generateCodeFrame(source, 0, 23)) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index a501e50eda397b..6d759fb554a98b 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -543,7 +543,17 @@ export function generateCodeFrame( for (let i = 0; i < lines.length; i++) { count += lines[i].length if (count >= start) { - for (let j = i - range; j <= i + range || end > count; j++) { + // `j < lines.length` bounds the second disjunct. Without it, once `j` + // runs past the last line the `continue` below skips the `count` + // update, so `count` freezes while `end` stays larger and the loop + // never terminates. `count` advances by one per line break, but `end` + // is clamped to `source.length`, which counts two characters per CRLF + // -- so on a CRLF source `end` can exceed anything `count` reaches. + for ( + let j = i - range; + j <= i + range || (end > count && j < lines.length); + j++ + ) { if (j < 0 || j >= lines.length) continue const line = j + 1 const lineLength = lines[j].length