Skip to content

Commit b092880

Browse files
committed
fix: Run Babel after OXC to fix React source locations
@rolldown/plugin-babel has enforce:'pre' by default, making it run before @vitejs/plugin-react (OXC). When the source location Babel plugin inserts lines, OXC sees modified line numbers and embeds wrong source positions in jsxDEV() calls. Override enforce to undefined so Babel runs after OXC, preserving correct source maps.
1 parent 2d312d2 commit b092880

2 files changed

Lines changed: 97 additions & 28 deletions

File tree

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

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,84 @@
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 { or (
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 after = lines[j].substring(arrowIdx + 2).trim();
51+
if (after.startsWith('{') || after.startsWith('(')) {
52+
return { line: j + 1, column: arrowIdx + 3 };
53+
}
54+
if (j + 1 < lines.length) {
55+
return { line: j + 2, column: 1 };
56+
}
57+
}
58+
}
59+
}
60+
}
61+
return null;
62+
}
63+
964
/**
1065
* 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.
66+
* This is used to make the source location of the function available in the browser in development mode.
1267
* The name __debugSourceDefine is prefixed by __ to mark this is not a public API.
68+
*
69+
* Uses the original source file to determine correct line numbers,
70+
* since Babel may be running on OXC-transformed code with different positions.
1371
*/
14-
function addDebugInfo(path, name, filename, loc) {
15-
const lineNumber = loc.start.line;
16-
const columnNumber = loc.start.column + 1;
72+
function addDebugInfo(path, name, filename) {
73+
const loc = findFunctionLine(filename, name);
74+
if (!loc) {
75+
return;
76+
}
1777
const debugSourceMember = t.memberExpression(t.identifier(name), t.identifier('__debugSourceDefine'));
1878
const debugSourceDefine = t.objectExpression([
1979
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))
80+
t.objectProperty(t.identifier('lineNumber'), t.numericLiteral(loc.line)),
81+
t.objectProperty(t.identifier('columnNumber'), t.numericLiteral(loc.column))
2282
]);
2383
const assignment = t.expressionStatement(t.assignmentExpression('=', debugSourceMember, debugSourceDefine));
2484
const condition = t.binaryExpression(
@@ -48,15 +108,13 @@ export function addFunctionComponentSourceLocationBabel() {
48108
}
49109

50110
const filename = state.file.opts.filename;
51-
if (declaration?.init?.body?.loc) {
52-
addDebugInfo(path, name, filename, declaration.init.body.loc);
53-
}
111+
addDebugInfo(path, name, filename);
54112
});
55113
},
56114

57115
FunctionDeclaration(path, state) {
58116
// Finds declarations such as
59-
// functio Foo() { return <div/>; }
117+
// function Foo() { return <div/>; }
60118
// export function Bar() { return <span>Hello</span>;}
61119

62120
// and writes a Foo.__debugSourceDefine= {..} after it, referring to the start of the function body
@@ -66,9 +124,7 @@ export function addFunctionComponentSourceLocationBabel() {
66124
return;
67125
}
68126
const filename = state.file.opts.filename;
69-
if (node.body.loc) {
70-
addDebugInfo(path, name, filename, node.body.loc);
71-
}
127+
addDebugInfo(path, name, filename);
72128
}
73129
}
74130
};

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)