Skip to content

Commit 20d4bf1

Browse files
committed
Merge branch 'develop' into manual-develop-sync
# Conflicts: # packages/bundler-plugins/package.json
2 parents b3342e3 + 852a409 commit 20d4bf1

14 files changed

Lines changed: 511 additions & 243 deletions

File tree

‎dev-packages/bundler-plugin-integration-tests/fixtures/vite8/component-annotation-next.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => {
3434
"data-sentry-component": "App",
3535
children: [/* @__PURE__ */ jsxDEV(ComponentA, {}, void 0, false, {
3636
fileName: _jsxFileName,
37-
lineNumber: 4,
37+
lineNumber: 6,
3838
columnNumber: 7
3939
}, this), ";"]
4040
}, void 0, true, {
4141
fileName: _jsxFileName,
42-
lineNumber: 3,
43-
columnNumber: 10
42+
lineNumber: 5,
43+
columnNumber: 5
4444
}, this);
4545
}
4646
console.log(App());

‎packages/bundler-plugins/package.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@
105105
}
106106
},
107107
"dependencies": {
108-
"@babel/core": "^7.18.5",
109108
"@sentry/core": "11.0.0",
110109
"dotenv": "^17.4.2",
111110
"glob": "^13.0.6",
@@ -115,10 +114,14 @@
115114
"supports-color": "^8.1.1"
116115
},
117116
"peerDependencies": {
117+
"@babel/core": "^7.18.5",
118118
"rollup": ">=3.2.0",
119119
"webpack": ">=5.1.0"
120120
},
121121
"peerDependenciesMeta": {
122+
"@babel/core": {
123+
"optional": true
124+
},
122125
"rollup": {
123126
"optional": true
124127
},
@@ -127,6 +130,7 @@
127130
}
128131
},
129132
"devDependencies": {
133+
"@babel/core": "^7.18.5",
130134
"@babel/preset-react": "^7.23.3",
131135
"@types/babel__core": "^7.20.5",
132136
"@types/node": "^18.6.3",

‎packages/bundler-plugins/src/core/component-annotation-oxc-ast.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,10 @@ export type ComponentAnnotationTransformMeta = {
5959
magicString?: MagicStringLike;
6060
};
6161

62-
export type ComponentAnnotationTransformResult =
63-
| {
64-
code: string;
65-
map?: SourceMap | string;
66-
}
67-
| null
68-
| undefined;
62+
export type ComponentAnnotationTransformResult = {
63+
code: string;
64+
map?: SourceMap | string;
65+
} | null;
6966

7067
export function isAstNode(value: unknown): value is AstNode {
7168
return isObjectLike(value) && typeof value.type === 'string';

‎packages/bundler-plugins/src/core/component-annotation-oxc-jsx.ts‎

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ import type {
1616
import { isAstNode, isObjectLike } from './component-annotation-oxc-ast';
1717

1818
const UNKNOWN_ELEMENT_NAME = 'unknown';
19+
const REACT_NATIVE_ELEMENTS = new Set([
20+
'Image',
21+
'Text',
22+
'View',
23+
'ScrollView',
24+
'TextInput',
25+
'TouchableOpacity',
26+
'TouchableHighlight',
27+
'TouchableWithoutFeedback',
28+
'FlatList',
29+
'SectionList',
30+
'ActivityIndicator',
31+
'Button',
32+
'Switch',
33+
'Modal',
34+
'SafeAreaView',
35+
'StatusBar',
36+
'KeyboardAvoidingView',
37+
'RefreshControl',
38+
'Picker',
39+
'Slider',
40+
]);
1941
const WEB_ATTRIBUTE_NAMES = [WEB_ELEMENT_NAME, WEB_COMPONENT_NAME, WEB_SOURCE_FILE_NAME] as const;
2042
const WEB_ATTRIBUTE_NAME_SET = new Set<string>(WEB_ATTRIBUTE_NAMES);
2143

@@ -140,20 +162,79 @@ export function addPendingAttributes(
140162
return;
141163
}
142164

143-
const insertion =
144-
pendingInsertion ??
145-
insertionsByOffset
146-
.set(offset, {
147-
offset,
148-
attributeValues: new Map(),
149-
})
150-
.get(offset);
165+
const insertion = getOrCreateInsertion(insertionsByOffset, offset);
151166

152167
for (const [name, value] of attributes) {
153-
insertion?.attributeValues.set(name, value);
168+
insertion.attributeValues.set(name, value);
154169
}
155170
}
156171

172+
/**
173+
* Returns `true` when the element is an HTML element, because HTML mode only
174+
* annotates the first HTML elements below a component root.
175+
*/
176+
export function addPendingHtmlAttribute(
177+
code: string,
178+
openingElement: JSXOpeningElementNode,
179+
componentName: string,
180+
ignoredComponents: string[],
181+
fragmentContext: FragmentContext,
182+
insertionsByOffset: Map<number, PendingInsertion>,
183+
): boolean {
184+
if (isReactFragment(openingElement, fragmentContext)) {
185+
return false;
186+
}
187+
188+
const elementName = getJSXName(openingElement.name);
189+
190+
if (!isHtmlElement(elementName)) {
191+
return false;
192+
}
193+
194+
if (ignoredComponents.includes(componentName) || ignoredComponents.includes(elementName)) {
195+
return true;
196+
}
197+
198+
const offset = getInsertionOffset(code, openingElement);
199+
if (offset === null) {
200+
return true;
201+
}
202+
203+
if (
204+
getExistingAttributeNames(openingElement).has(WEB_COMPONENT_NAME) ||
205+
insertionsByOffset.get(offset)?.attributeValues.has(WEB_COMPONENT_NAME)
206+
) {
207+
return true;
208+
}
209+
210+
getOrCreateInsertion(insertionsByOffset, offset).attributeValues.set(WEB_COMPONENT_NAME, componentName);
211+
212+
return true;
213+
}
214+
215+
function isHtmlElement(elementName: string): boolean {
216+
if (elementName === UNKNOWN_ELEMENT_NAME) {
217+
return false;
218+
}
219+
220+
if (elementName.charAt(0) === elementName.charAt(0).toLowerCase()) {
221+
return true;
222+
}
223+
224+
return REACT_NATIVE_ELEMENTS.has(elementName);
225+
}
226+
227+
function getOrCreateInsertion(insertionsByOffset: Map<number, PendingInsertion>, offset: number): PendingInsertion {
228+
let insertion = insertionsByOffset.get(offset);
229+
230+
if (!insertion) {
231+
insertion = { offset, attributeValues: new Map() };
232+
insertionsByOffset.set(offset, insertion);
233+
}
234+
235+
return insertion;
236+
}
237+
157238
export function toAttributeInsertions(insertionsByOffset: Map<number, PendingInsertion>): AttributeInsertion[] {
158239
return [...insertionsByOffset.values()].map(({ offset, attributeValues }) => ({
159240
offset,
@@ -181,7 +262,8 @@ function getOrderedAttributes(attributeValues: ReadonlyMap<string, string>): Com
181262

182263
for (const name of WEB_ATTRIBUTE_NAMES) {
183264
const value = attributeValues.get(name);
184-
if (value) {
265+
// HTML mode writes an empty component name for anonymous classes.
266+
if (value !== undefined) {
185267
attributes.push([name, value]);
186268
}
187269
}

‎packages/bundler-plugins/src/core/component-annotation-oxc-walk.ts‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AstNode, AttributeInsertion, FragmentContext, JSXRootNode } from './component-annotation-oxc-ast';
22
import {
33
addPendingAttributes,
4+
addPendingHtmlAttribute,
45
getStringName,
56
isJSXElement,
67
isJSXRoot,
@@ -16,20 +17,54 @@ export function collectOxcComponentAnnotationInsertions(
1617
ast: AstNode,
1718
ignoredComponents: string[],
1819
sourceFileName: string,
20+
injectIntoHtml: boolean,
1921
): AttributeInsertion[] {
2022
const fragmentContext = collectFragmentContext(ast);
2123
const components = collectComponentJSXRoots(ast);
2224
const insertionsByOffset = new Map<number, { offset: number; attributeValues: Map<string, string> }>();
2325

2426
for (const component of components) {
2527
for (const root of component.roots) {
26-
processJSX(code, root, component.name, ignoredComponents, fragmentContext, sourceFileName, insertionsByOffset);
28+
if (injectIntoHtml) {
29+
processHtmlJSX(code, root, component.name, ignoredComponents, fragmentContext, insertionsByOffset);
30+
} else {
31+
processJSX(code, root, component.name, ignoredComponents, fragmentContext, sourceFileName, insertionsByOffset);
32+
}
2733
}
2834
}
2935

3036
return toAttributeInsertions(insertionsByOffset);
3137
}
3238

39+
function processHtmlJSX(
40+
code: string,
41+
node: JSXRootNode,
42+
componentName: string,
43+
ignoredComponents: string[],
44+
fragmentContext: FragmentContext,
45+
insertionsByOffset: Map<number, { offset: number; attributeValues: Map<string, string> }>,
46+
): void {
47+
if (
48+
isJSXElement(node) &&
49+
addPendingHtmlAttribute(
50+
code,
51+
node.openingElement,
52+
componentName,
53+
ignoredComponents,
54+
fragmentContext,
55+
insertionsByOffset,
56+
)
57+
) {
58+
return;
59+
}
60+
61+
for (const child of node.children ?? []) {
62+
if (isJSXRoot(child)) {
63+
processHtmlJSX(code, child, componentName, ignoredComponents, fragmentContext, insertionsByOffset);
64+
}
65+
}
66+
}
67+
3368
function getJSXRootsFromReturnArgument(argument: unknown): JSXRootNode[] {
3469
if (isJSXRoot(argument)) {
3570
return [argument];

‎packages/bundler-plugins/src/core/component-annotation-oxc.ts‎

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function getOxcParseAstAsync(): Promise<ParseAstAsync | null> {
4343
return oxcParseAstAsyncPromise;
4444
}
4545

46-
// Keep this as a superset of JSX tag starts Babel can annotate, because a miss suppresses Babel fallback.
46+
// Keep this as a superset of all JSX tag starts, because a miss skips the file.
4747
const JSX_TAG_START_REGEXP = /<[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}.:-]*|<>/u;
4848
const JSX_FILE_REGEXP = /\.[jt]sx$/;
4949

@@ -98,37 +98,26 @@ function getMagicString(
9898

9999
async function annotateWithOxcParser(
100100
code: string,
101-
id: string,
101+
idWithoutQueryAndHash: string,
102102
ignoredComponents: string[],
103103
parseAstAsync: ParseAstAsync,
104+
injectIntoHtml: boolean,
104105
meta?: ComponentAnnotationTransformMeta,
105106
): Promise<ComponentAnnotationTransformResult> {
106-
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);
107-
108-
if (
109-
!idWithoutQueryAndHash ||
110-
!isAnnotationFile(idWithoutQueryAndHash) ||
111-
!shouldTryParse(code) ||
112-
shouldSkipIncompatibleFile(idWithoutQueryAndHash)
113-
) {
114-
return null;
115-
}
116-
117107
const ast = await parseAstAsync(code, {
118108
lang: idWithoutQueryAndHash.endsWith('.jsx') ? 'jsx' : 'tsx',
119109
});
120110

121-
// Fall through to the Babel fallback (return undefined, not null) when the parser yields an
122-
// unexpected result, mirroring the catch path — null would suppress Babel and drop annotations.
123111
if (!isAstNode(ast)) {
124-
return undefined;
112+
return null;
125113
}
126114

127115
const insertions = collectOxcComponentAnnotationInsertions(
128116
code,
129117
ast,
130118
ignoredComponents,
131119
path.basename(idWithoutQueryAndHash),
120+
injectIntoHtml,
132121
);
133122

134123
if (insertions.length === 0) {
@@ -147,8 +136,9 @@ async function annotateWithOxcParser(
147136

148137
return {
149138
code: magicString.toString(),
139+
// No `file`, because magic-string would then make `source` relative to it
140+
// and drop the directory from `sources`.
150141
map: magicString.generateMap?.({
151-
file: id,
152142
source: idWithoutQueryAndHash,
153143
includeContent: true,
154144
hires: true,
@@ -159,6 +149,7 @@ async function annotateWithOxcParser(
159149
export function createOxcComponentNameAnnotateHooks(
160150
ignoredComponents: string[],
161151
getParseAstAsync: () => Promise<ParseAstAsync | null>,
152+
injectIntoHtml = false,
162153
): {
163154
transform(
164155
code: string,
@@ -168,16 +159,35 @@ export function createOxcComponentNameAnnotateHooks(
168159
} {
169160
return {
170161
async transform(code, id, meta) {
162+
const idWithoutQueryAndHash = stripQueryAndHashFromPath(id);
163+
164+
if (
165+
!idWithoutQueryAndHash ||
166+
!isAnnotationFile(idWithoutQueryAndHash) ||
167+
!shouldTryParse(code) ||
168+
shouldSkipIncompatibleFile(idWithoutQueryAndHash)
169+
) {
170+
return null;
171+
}
172+
171173
try {
172174
const parseAstAsync = await getParseAstAsync();
173175

174176
if (!parseAstAsync) {
175-
return undefined;
177+
return null;
176178
}
177179

178-
return await annotateWithOxcParser(code, id, ignoredComponents, parseAstAsync, meta);
180+
return await annotateWithOxcParser(
181+
code,
182+
idWithoutQueryAndHash,
183+
ignoredComponents,
184+
parseAstAsync,
185+
injectIntoHtml,
186+
meta,
187+
);
179188
} catch {
180-
return undefined;
189+
// Leave files the parser cannot read unchanged. The bundler reports the syntax error.
190+
return null;
181191
}
182192
},
183193
};

0 commit comments

Comments
 (0)