Skip to content

Commit 748ff8a

Browse files
authored
fix: Fix React source locations for Vite 8 compatibility (#24140)
Two changes to fix source location tracking when using Vite 8/Rolldown: 1. Replace @rolldown/plugin-babel with a custom Vite plugin using @babel/core directly. The @rolldown/plugin-babel hardcodes enforce:'pre', making Babel run before @vitejs/plugin-react (OXC). This caused OXC to see Babel-modified code and produce wrong line numbers in jsxDEV() source info. The custom plugin uses enforce:'post' so Babel runs after OXC. 2. Update addFunctionComponentSourceLocationBabel to read the original source file from disk instead of using Babel AST positions. When running after OXC, Babel's AST loc values refer to the transformed code, not the original. Reading the file directly ensures __debugSourceDefine always contains correct original line numbers.
1 parent 5bdf472 commit 748ff8a

4 files changed

Lines changed: 101 additions & 30 deletions

File tree

flow-build-tools/src/main/resources/plugins/react-function-location-plugin/react-function-location-plugin.js

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,88 @@
11
import * as t from '@babel/types';
2+
import { readFileSync } from 'fs';
23

34
export function addFunctionComponentSourceLocationBabel() {
45
function isReactFunctionName(name) {
56
// A React component function always starts with a Capital letter
67
return name && name.match(/^[A-Z].*/);
78
}
89

10+
// Cache original file contents for finding correct line numbers.
11+
// When running after OXC (enforce: 'post'), Babel's AST positions
12+
// refer to the transformed code, not the original source. We read
13+
// the original file to get correct positions.
14+
const originalSources = {};
15+
16+
function getOriginalLines(filename) {
17+
if (!originalSources[filename]) {
18+
try {
19+
originalSources[filename] = readFileSync(filename, 'utf-8').split('\n');
20+
} catch {
21+
originalSources[filename] = [];
22+
}
23+
}
24+
return originalSources[filename];
25+
}
26+
27+
function findFunctionLine(filename, functionName) {
28+
const lines = getOriginalLines(filename);
29+
for (let i = 0; i < lines.length; i++) {
30+
// Match "function FunctionName(" or "const/let/var FunctionName ="
31+
const funcMatch = lines[i].match(new RegExp(`\\bfunction\\s+${functionName}\\s*\\(`));
32+
const constMatch = lines[i].match(new RegExp(`\\b(?:const|let|var)\\s+${functionName}\\s*=`));
33+
if (funcMatch) {
34+
// Find the opening brace of the function body
35+
const braceCol = lines[i].indexOf('{', funcMatch.index + funcMatch[0].length);
36+
if (braceCol >= 0) {
37+
return { line: i + 1, column: braceCol + 1 };
38+
}
39+
// Brace might be on a following line
40+
for (let j = i + 1; j < lines.length; j++) {
41+
const bc = lines[j].indexOf('{');
42+
if (bc >= 0) return { line: j + 1, column: bc + 1 };
43+
}
44+
}
45+
if (constMatch) {
46+
// Find arrow function body: look for => and then the body start
47+
for (let j = i; j < Math.min(i + 5, lines.length); j++) {
48+
const arrowIdx = lines[j].indexOf('=>');
49+
if (arrowIdx >= 0) {
50+
const afterArrow = lines[j].substring(arrowIdx + 2);
51+
const trimmed = afterArrow.trim();
52+
if (trimmed.length > 0) {
53+
// Body starts on same line as =>
54+
const bodyStart = arrowIdx + 2 + afterArrow.indexOf(trimmed.charAt(0));
55+
return { line: j + 1, column: bodyStart + 1 };
56+
}
57+
// Body starts on next line
58+
if (j + 1 < lines.length) {
59+
return { line: j + 2, column: 1 };
60+
}
61+
}
62+
}
63+
}
64+
}
65+
return null;
66+
}
67+
968
/**
1069
* Writes debug info as Name.__debugSourceDefine={...} after the given statement ("path").
11-
* This is used to make the source location of the function (defined by the loc parameter) available in the browser in development mode.
70+
* This is used to make the source location of the function available in the browser in development mode.
1271
* The name __debugSourceDefine is prefixed by __ to mark this is not a public API.
72+
*
73+
* Uses the original source file to determine correct line numbers,
74+
* since Babel may be running on OXC-transformed code with different positions.
1375
*/
14-
function addDebugInfo(path, name, filename, loc) {
15-
const lineNumber = loc.start.line;
16-
const columnNumber = loc.start.column + 1;
76+
function addDebugInfo(path, name, filename) {
77+
const loc = findFunctionLine(filename, name);
78+
if (!loc) {
79+
return;
80+
}
1781
const debugSourceMember = t.memberExpression(t.identifier(name), t.identifier('__debugSourceDefine'));
1882
const debugSourceDefine = t.objectExpression([
1983
t.objectProperty(t.identifier('fileName'), t.stringLiteral(filename)),
20-
t.objectProperty(t.identifier('lineNumber'), t.numericLiteral(lineNumber)),
21-
t.objectProperty(t.identifier('columnNumber'), t.numericLiteral(columnNumber))
84+
t.objectProperty(t.identifier('lineNumber'), t.numericLiteral(loc.line)),
85+
t.objectProperty(t.identifier('columnNumber'), t.numericLiteral(loc.column))
2286
]);
2387
const assignment = t.expressionStatement(t.assignmentExpression('=', debugSourceMember, debugSourceDefine));
2488
const condition = t.binaryExpression(
@@ -48,15 +112,13 @@ export function addFunctionComponentSourceLocationBabel() {
48112
}
49113

50114
const filename = state.file.opts.filename;
51-
if (declaration?.init?.body?.loc) {
52-
addDebugInfo(path, name, filename, declaration.init.body.loc);
53-
}
115+
addDebugInfo(path, name, filename);
54116
});
55117
},
56118

57119
FunctionDeclaration(path, state) {
58120
// Finds declarations such as
59-
// functio Foo() { return <div/>; }
121+
// function Foo() { return <div/>; }
60122
// export function Bar() { return <span>Hello</span>;}
61123

62124
// and writes a Foo.__debugSourceDefine= {..} after it, referring to the start of the function body
@@ -66,9 +128,7 @@ export function addFunctionComponentSourceLocationBabel() {
66128
return;
67129
}
68130
const filename = state.file.opts.filename;
69-
if (node.body.loc) {
70-
addDebugInfo(path, name, filename, node.body.loc);
71-
}
131+
addDebugInfo(path, name, filename);
72132
}
73133
}
74134
};

flow-build-tools/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ void getDefaultDevDependencies_includesAllDependencies_whenUsingVite() {
170170
expectedDependencies.add("strip-css-comments");
171171
expectedDependencies.add("@babel/core");
172172
expectedDependencies.add("@babel/preset-react");
173-
expectedDependencies.add("@rolldown/plugin-babel");
174173
expectedDependencies.add("@types/react");
175174
expectedDependencies.add("@types/react-dom");
176175
expectedDependencies.add("@preact/signals-react-transform");

flow-server/src/main/resources/com/vaadin/flow/server/frontend/dependencies/vite/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"@rollup/pluginutils": "5.3.0",
1717
"@babel/core": "7.29.0",
1818
"@babel/preset-react": "7.28.5",
19-
"@rolldown/plugin-babel": "0.2.1",
2019
"rollup-plugin-visualizer": "7.0.1",
2120
"rollup-plugin-brotli": "3.1.0",
2221
"vite-plugin-checker": "0.12.0",

flow-server/src/main/resources/vite.generated.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export { default as useLocalWebComponents } from '#buildFolder#/plugins/vite-plu
3232

3333
import { visualizer } from 'rollup-plugin-visualizer';
3434
import reactPlugin from '@vitejs/plugin-react';
35-
import babel from '@rolldown/plugin-babel';
35+
import { transformSync } from '@babel/core';
3636
//#tailwindcssVitePluginImport#
3737

3838
//#vitePluginFileSystemRouterImport#
@@ -533,20 +533,33 @@ export const vaadinConfig: UserConfigFn = (env) => {
533533
include: '**/*.tsx',
534534
jsxImportSource: productionMode ? 'react' : 'Frontend/generated/jsx-dev-transform',
535535
}),
536-
// Babel plugins for source location info and signals transform
537-
// (separate from reactPlugin since v6 uses OXC instead of Babel for JSX)
538-
babel({
539-
include: '**/*.tsx',
540-
plugins: [
541-
!productionMode && addFunctionComponentSourceLocationBabel(),
542-
[
543-
'module:@preact/signals-react-transform',
544-
{
545-
mode: 'all' // Needed to include translations which do not use something.value
546-
}
547-
]
548-
].filter(Boolean)
549-
}),
536+
// Babel plugins for source location info and signals transform.
537+
// Must run AFTER reactPlugin (OXC) so that source locations are correct.
538+
// Uses a custom plugin instead of @rolldown/plugin-babel because that
539+
// plugin hardcodes enforce:'pre' which cannot be overridden.
540+
{
541+
name: 'vaadin-babel-post',
542+
enforce: 'post' as const,
543+
transform(code: string, id: string) {
544+
if (!id.endsWith('.tsx')) return null;
545+
const result = transformSync(code, {
546+
filename: id,
547+
plugins: [
548+
!productionMode && addFunctionComponentSourceLocationBabel(),
549+
[
550+
'module:@preact/signals-react-transform',
551+
{
552+
mode: 'all' // Needed to include translations which do not use something.value
553+
}
554+
]
555+
].filter(Boolean),
556+
sourceMaps: true,
557+
sourceFileName: id,
558+
});
559+
if (!result?.code) return null;
560+
return { code: result.code, map: result.map };
561+
},
562+
},
550563
//#tailwindcssVitePlugin#
551564
productionMode && vaadinI18n({
552565
cwd: __dirname,

0 commit comments

Comments
 (0)