diff --git a/.craft.yml b/.craft.yml
index 85179f1c..e3db425c 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -9,6 +9,7 @@ requireNames:
- /^sentry-rollup-plugin-.*\.tgz$/
- /^sentry-vite-plugin-.*\.tgz$/
- /^sentry-webpack-plugin-.*\.tgz$/
+ - /^sentry-component-annotate-plugin-.*\.tgz$/
targets:
- name: github
includeNames: /^sentry-.*.tgz$/
diff --git a/packages/component-annotate-plugin/.babelrc.json b/packages/component-annotate-plugin/.babelrc.json
new file mode 100644
index 00000000..0c20c06e
--- /dev/null
+++ b/packages/component-annotate-plugin/.babelrc.json
@@ -0,0 +1,3 @@
+{
+ "presets": ["@babel/env", "@babel/typescript"]
+}
diff --git a/packages/component-annotate-plugin/.eslintrc.js b/packages/component-annotate-plugin/.eslintrc.js
new file mode 100644
index 00000000..42b35d84
--- /dev/null
+++ b/packages/component-annotate-plugin/.eslintrc.js
@@ -0,0 +1,20 @@
+const jestPackageJson = require("jest/package.json");
+
+/** @type {import('eslint').ESLint.Options} */
+module.exports = {
+ root: true,
+ extends: ["@sentry-internal/eslint-config/jest", "@sentry-internal/eslint-config/base"],
+ ignorePatterns: [".eslintrc.js", "dist", "jest.config.js", "rollup.config.js"],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: ["./src/tsconfig.json", "./test/tsconfig.json"],
+ },
+ env: {
+ node: true,
+ },
+ settings: {
+ jest: {
+ version: jestPackageJson.version,
+ },
+ },
+};
diff --git a/packages/component-annotate-plugin/.gitignore b/packages/component-annotate-plugin/.gitignore
new file mode 100644
index 00000000..36d3a9c3
--- /dev/null
+++ b/packages/component-annotate-plugin/.gitignore
@@ -0,0 +1,2 @@
+dist
+.DS_Store
diff --git a/packages/component-annotate-plugin/LICENSE b/packages/component-annotate-plugin/LICENSE
new file mode 100644
index 00000000..042360af
--- /dev/null
+++ b/packages/component-annotate-plugin/LICENSE
@@ -0,0 +1,29 @@
+# MIT License
+
+Copyright (c) 2024, Sentry
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+- Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/component-annotate-plugin/README.md b/packages/component-annotate-plugin/README.md
new file mode 100644
index 00000000..562b656c
--- /dev/null
+++ b/packages/component-annotate-plugin/README.md
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+# Sentry Component Annotate Plugin (Beta)
+
+[](https://www.npmjs.com/package/@sentry/component-annotate-plugin)
+[](https://www.npmjs.com/package/@sentry/component-annotate-plugin)
+[](https://www.npmjs.com/package/@component-annotate-plugin)
+
+This plugin is currently in beta. Please help us improve by [reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript-bundler-plugins/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).
+
+A Babel plugin that automatically annotates your output DOM with their respective frontend component names.
+This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in breadcrumbs and performance monitoring.
+Please note that your Sentry JavaScript SDK version must be at least `7.91.0` to take advantage of these features.
+Currently, this plugin only works with React, and will exclusively parse `.jsx` and `.tsx` files.
+
+### Note
+
+This plugin comes included in Sentry's bundler plugins, alongside many other features to improve your Sentry workflow.
+It can be downloaded individually, but it is recommended that you install the bundler plugins for your respective bundler, and enable this feature through the config object.
+
+Check out the supported bundler plugin packages for installation instructions:
+
+- [Rollup](https://www.npmjs.com/package/@sentry/rollup-plugin)
+- [Vite](https://www.npmjs.com/package/@sentry/vite-plugin)
+- [Webpack](https://www.npmjs.com/package/@sentry/webpack-plugin)
+- esbuild: Not currently supported
+
+## Installation
+
+Using npm:
+
+```bash
+npm install @sentry/component-annotate-plugin --save-dev
+```
+
+Using yarn:
+
+```bash
+yarn add @sentry/component-annotate-plugin --dev
+```
+
+Using pnpm:
+
+```bash
+pnpm install @sentry/component-annotate-plugin --dev
+```
+
+## Example
+
+```js
+// babel.config.js
+
+{
+ // ... other config above ...
+
+ plugins: [
+ // Put this plugin before any other plugins you have that transform JSX code
+ ['@sentry/component-annotate-plugin']
+ ],
+}
+```
+
+Or alternatively, configure the plugin by directly importing it:
+
+```js
+// babel.config.js
+
+import {componentNameAnnotatePlugin} from '@sentry/component-annotate-plugin';
+
+{
+ // ... other config above ...
+
+ plugins: [
+ // Put this plugin before any other plugins you have that transform JSX code
+ [componentNameAnnotatePlugin]
+ ],
+}
+```
+
+## More information
+
+- [Sentry Documentation](https://docs.sentry.io/quickstart/)
+- [Sentry Discord](https://discord.gg/Ww9hbqr)
+- [Sentry Stackoverflow](http://stackoverflow.com/questions/tagged/sentry)
diff --git a/packages/component-annotate-plugin/jest.config.js b/packages/component-annotate-plugin/jest.config.js
new file mode 100644
index 00000000..160178bb
--- /dev/null
+++ b/packages/component-annotate-plugin/jest.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ testEnvironment: "node",
+ transform: {
+ "^.+\\.(t|j)sx?$": ["@swc/jest"],
+ },
+};
diff --git a/packages/component-annotate-plugin/package.json b/packages/component-annotate-plugin/package.json
new file mode 100644
index 00000000..a995e46a
--- /dev/null
+++ b/packages/component-annotate-plugin/package.json
@@ -0,0 +1,81 @@
+{
+ "name": "@sentry/component-annotate-plugin",
+ "version": "2.10.3",
+ "description": "A Babel plugin that annotates frontend components with additional data to enrich the experience in Sentry",
+ "repository": "git://github.com/getsentry/sentry-javascript-bundler-plugins.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/component-annotate-plugin",
+ "author": "Sentry",
+ "license": "MIT",
+ "keywords": [
+ "Sentry",
+ "React",
+ "bundler",
+ "plugin",
+ "babel",
+ "component",
+ "annotate"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "files": [
+ "dist"
+ ],
+ "exports": {
+ ".": {
+ "import": "./dist/esm/index.mjs",
+ "require": "./dist/cjs/index.js",
+ "types": "./dist/types/index.d.ts"
+ }
+ },
+ "main": "dist/cjs/index.js",
+ "module": "dist/esm/index.mjs",
+ "types": "dist/types/index.d.ts",
+ "scripts": {
+ "build": "rimraf ./out && run-p build:rollup build:types",
+ "build:watch": "run-p build:rollup:watch build:types:watch",
+ "build:rollup": "rollup --config rollup.config.js",
+ "build:rollup:watch": "rollup --config rollup.config.js --watch --no-watch.clearScreen",
+ "build:types": "tsc --project types.tsconfig.json",
+ "build:types:watch": "tsc --project types.tsconfig.json --watch --preserveWatchOutput",
+ "build:npm": "npm pack",
+ "check:types": "run-p check:types:src check:types:test",
+ "check:types:src": "tsc --project ./src/tsconfig.json --noEmit",
+ "check:types:test": "tsc --project ./test/tsconfig.json --noEmit",
+ "clean": "run-s clean:build",
+ "clean:all": "run-p clean clean:deps",
+ "clean:build": "rimraf ./dist *.tgz",
+ "clean:deps": "rimraf node_modules",
+ "test": "jest",
+ "lint": "eslint ./src ./test",
+ "prepack": "ts-node ./src/prepack.ts"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "@babel/core": "7.18.5",
+ "@babel/preset-env": "7.18.2",
+ "@babel/preset-react": "^7.23.3",
+ "@babel/preset-typescript": "7.17.12",
+ "@rollup/plugin-babel": "5.3.1",
+ "@rollup/plugin-node-resolve": "13.3.0",
+ "@sentry-internal/eslint-config": "2.10.3",
+ "@sentry-internal/sentry-bundler-plugin-tsconfig": "2.10.3",
+ "@swc/core": "^1.2.205",
+ "@swc/jest": "^0.2.21",
+ "@types/jest": "^28.1.3",
+ "@types/node": "^18.6.3",
+ "@types/uuid": "^9.0.1",
+ "eslint": "^8.18.0",
+ "jest": "^28.1.1",
+ "rimraf": "^3.0.2",
+ "rollup": "2.75.7",
+ "ts-node": "^10.9.1",
+ "typescript": "^4.7.4"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+}
diff --git a/packages/component-annotate-plugin/rollup.config.js b/packages/component-annotate-plugin/rollup.config.js
new file mode 100644
index 00000000..95b11585
--- /dev/null
+++ b/packages/component-annotate-plugin/rollup.config.js
@@ -0,0 +1,42 @@
+import resolve from "@rollup/plugin-node-resolve";
+import babel from "@rollup/plugin-babel";
+import packageJson from "./package.json";
+import modulePackage from "module";
+
+const input = ["src/index.ts"];
+
+const extensions = [".ts"];
+
+export default {
+ input,
+ external: [...Object.keys(packageJson.dependencies), ...modulePackage.builtinModules],
+ onwarn: (warning) => {
+ throw new Error(warning.message); // Warnings are usually high-consequence for us so let's throw to catch them
+ },
+ plugins: [
+ resolve({
+ extensions,
+ rootDir: "./src",
+ preferBuiltins: true,
+ }),
+ babel({
+ extensions,
+ babelHelpers: "bundled",
+ include: ["src/**/*"],
+ }),
+ ],
+ output: [
+ {
+ file: packageJson.module,
+ format: "esm",
+ exports: "named",
+ sourcemap: true,
+ },
+ {
+ file: packageJson.main,
+ format: "cjs",
+ exports: "named",
+ sourcemap: true,
+ },
+ ],
+};
diff --git a/packages/component-annotate-plugin/src/constants.ts b/packages/component-annotate-plugin/src/constants.ts
new file mode 100644
index 00000000..51eac57d
--- /dev/null
+++ b/packages/component-annotate-plugin/src/constants.ts
@@ -0,0 +1,146 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020 Engineering at FullStory
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+export const KNOWN_INCOMPATIBLE_PLUGINS = [
+ // This module might be causing an issue preventing clicks. For safety, we won't run on this module.
+ "react-native-testfairy",
+ // This module checks for unexpected property keys and throws an exception.
+ "@react-navigation",
+];
+
+export const DEFAULT_IGNORED_ELEMENTS = [
+ "a",
+ "abbr",
+ "address",
+ "area",
+ "article",
+ "aside",
+ "audio",
+ "b",
+ "base",
+ "bdi",
+ "bdo",
+ "blockquote",
+ "body",
+ "br",
+ "button",
+ "canvas",
+ "caption",
+ "cite",
+ "code",
+ "col",
+ "colgroup",
+ "data",
+ "datalist",
+ "dd",
+ "del",
+ "details",
+ "dfn",
+ "dialog",
+ "div",
+ "dl",
+ "dt",
+ "em",
+ "embed",
+ "fieldset",
+ "figure",
+ "footer",
+ "form",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "head",
+ "header",
+ "hgroup",
+ "hr",
+ "html",
+ "i",
+ "iframe",
+ "img",
+ "input",
+ "ins",
+ "kbd",
+ "keygen",
+ "label",
+ "legend",
+ "li",
+ "link",
+ "main",
+ "map",
+ "mark",
+ "menu",
+ "menuitem",
+ "meter",
+ "nav",
+ "noscript",
+ "object",
+ "ol",
+ "optgroup",
+ "option",
+ "output",
+ "p",
+ "param",
+ "pre",
+ "progress",
+ "q",
+ "rb",
+ "rp",
+ "rt",
+ "rtc",
+ "ruby",
+ "s",
+ "samp",
+ "script",
+ "section",
+ "select",
+ "small",
+ "source",
+ "span",
+ "strong",
+ "style",
+ "sub",
+ "summary",
+ "sup",
+ "table",
+ "tbody",
+ "td",
+ "template",
+ "textarea",
+ "tfoot",
+ "th",
+ "thead",
+ "time",
+ "title",
+ "tr",
+ "track",
+ "u",
+ "ul",
+ "var",
+ "video",
+ "wbr",
+];
diff --git a/packages/component-annotate-plugin/src/index.ts b/packages/component-annotate-plugin/src/index.ts
new file mode 100644
index 00000000..894155c0
--- /dev/null
+++ b/packages/component-annotate-plugin/src/index.ts
@@ -0,0 +1,517 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020 Engineering at FullStory
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+/**
+ * The following code is based on the FullStory Babel plugin, but has been modified to work
+ * with Sentry products:
+ *
+ * - Added `sentry` to data properties, i.e `data-sentry-component`
+ * - Converted to TypeScript
+ * - Code cleanups
+ */
+
+import type * as Babel from "@babel/core";
+import type { PluginObj, PluginPass } from "@babel/core";
+
+import { DEFAULT_IGNORED_ELEMENTS, KNOWN_INCOMPATIBLE_PLUGINS } from "./constants";
+
+const webComponentName = "data-sentry-component";
+const webElementName = "data-sentry-element";
+const webSourceFileName = "data-sentry-source-file";
+
+const nativeComponentName = "dataSentryComponent";
+const nativeElementName = "dataSentryElement";
+const nativeSourceFileName = "dataSentrySourceFile";
+
+interface AnnotationOpts {
+ native?: boolean;
+ "annotate-fragments"?: boolean;
+ ignoreComponents?: IgnoredComponent[];
+}
+
+interface AnnotationPluginPass extends PluginPass {
+ opts: AnnotationOpts;
+}
+
+type IgnoredComponent = [file: string, component: string, element: string];
+
+type AnnotationPlugin = PluginObj;
+
+export function componentNameAnnotatePlugin({ types: t }: typeof Babel): AnnotationPlugin {
+ return {
+ visitor: {
+ FunctionDeclaration(path, state) {
+ if (!path.node.id || !path.node.id.name) {
+ return;
+ }
+ if (isKnownIncompatiblePluginFromState(state)) {
+ return;
+ }
+
+ functionBodyPushAttributes(
+ state.opts["annotate-fragments"] === true,
+ t,
+ path,
+ path.node.id.name,
+ sourceFileNameFromState(state),
+ attributeNamesFromState(state),
+ state.opts.ignoreComponents ?? []
+ );
+ },
+ ArrowFunctionExpression(path, state) {
+ // We're expecting a `VariableDeclarator` like `const MyComponent =`
+ const parent = path.parent;
+ if (
+ !parent ||
+ !("id" in parent) ||
+ !parent.id ||
+ !("name" in parent.id) ||
+ !parent.id.name
+ ) {
+ return;
+ }
+
+ if (isKnownIncompatiblePluginFromState(state)) {
+ return;
+ }
+
+ functionBodyPushAttributes(
+ state.opts["annotate-fragments"] === true,
+ t,
+ path,
+ parent.id.name,
+ sourceFileNameFromState(state),
+ attributeNamesFromState(state),
+ state.opts.ignoreComponents ?? []
+ );
+ },
+ ClassDeclaration(path, state) {
+ const name = path.get("id");
+ const properties = path.get("body").get("body");
+ const render = properties.find((prop) => {
+ return prop.isClassMethod() && prop.get("key").isIdentifier({ name: "render" });
+ });
+
+ if (!render || !render.traverse || isKnownIncompatiblePluginFromState(state)) {
+ return;
+ }
+
+ const ignoredComponents = state.opts.ignoreComponents ?? [];
+
+ render.traverse({
+ ReturnStatement(returnStatement) {
+ const arg = returnStatement.get("argument");
+
+ if (!arg.isJSXElement() && !arg.isJSXFragment()) {
+ return;
+ }
+
+ processJSX(
+ state.opts["annotate-fragments"] === true,
+ t,
+ arg,
+ name.node && name.node.name,
+ sourceFileNameFromState(state),
+ attributeNamesFromState(state),
+ ignoredComponents
+ );
+ },
+ });
+ },
+ },
+ };
+}
+
+function functionBodyPushAttributes(
+ annotateFragments: boolean,
+ t: typeof Babel.types,
+ path: Babel.NodePath,
+ componentName: string,
+ sourceFileName: string | undefined,
+ attributeNames: string[],
+ ignoredComponents: IgnoredComponent[]
+) {
+ let jsxNode: Babel.NodePath;
+
+ const functionBody = path.get("body").get("body");
+
+ if (
+ !("length" in functionBody) &&
+ functionBody.parent &&
+ (functionBody.parent.type === "JSXElement" || functionBody.parent.type === "JSXFragment")
+ ) {
+ const maybeJsxNode = functionBody.find((c) => {
+ return c.type === "JSXElement" || c.type === "JSXFragment";
+ });
+
+ if (!maybeJsxNode) {
+ return;
+ }
+
+ jsxNode = maybeJsxNode;
+ } else {
+ const returnStatement = functionBody.find((c) => {
+ return c.type === "ReturnStatement";
+ });
+ if (!returnStatement) {
+ return;
+ }
+
+ const arg = returnStatement.get("argument");
+ if (!arg) {
+ return;
+ }
+
+ if (Array.isArray(arg)) {
+ return;
+ }
+
+ if (!arg.isJSXFragment() && !arg.isJSXElement()) {
+ return;
+ }
+
+ jsxNode = arg;
+ }
+
+ if (!jsxNode) {
+ return;
+ }
+
+ processJSX(
+ annotateFragments,
+ t,
+ jsxNode,
+ componentName,
+ sourceFileName,
+ attributeNames,
+ ignoredComponents
+ );
+}
+
+function processJSX(
+ annotateFragments: boolean,
+ t: typeof Babel.types,
+ jsxNode: Babel.NodePath,
+ componentName: string | null,
+ sourceFileName: string | undefined,
+ attributeNames: string[],
+ ignoredComponents: IgnoredComponent[]
+) {
+ if (!jsxNode) {
+ return;
+ }
+
+ // NOTE: I don't know of a case where `openingElement` would have more than one item,
+ // but it's safer to always iterate
+ const paths = jsxNode.get("openingElement");
+ const openingElements = Array.isArray(paths) ? paths : [paths];
+
+ openingElements.forEach((openingElement) => {
+ applyAttributes(
+ t,
+ openingElement as Babel.NodePath,
+ componentName,
+ sourceFileName,
+ attributeNames,
+ ignoredComponents
+ );
+ });
+
+ let children = jsxNode.get("children");
+ // TODO: See why `Array.isArray` doesn't have correct behaviour here
+ if (children && !("length" in children)) {
+ // A single child was found, maybe a bit of static text
+ children = [children];
+ }
+
+ let shouldSetComponentName = annotateFragments;
+
+ children.forEach((child) => {
+ // Happens for some node types like plain text
+ if (!child.node) {
+ return;
+ }
+
+ // Children don't receive the data-component attribute so we pass null for componentName unless it's the first child of a Fragment with a node and `annotateFragments` is true
+ const openingElement = child.get("openingElement");
+ // TODO: Improve this. We never expect to have multiple opening elements
+ // but if it's possible, this should work
+ if (Array.isArray(openingElement)) {
+ return;
+ }
+
+ if (shouldSetComponentName && openingElement && openingElement.node) {
+ shouldSetComponentName = false;
+ processJSX(
+ annotateFragments,
+ t,
+ child,
+ componentName,
+ sourceFileName,
+ attributeNames,
+ ignoredComponents
+ );
+ } else {
+ processJSX(
+ annotateFragments,
+ t,
+ child,
+ null,
+ sourceFileName,
+ attributeNames,
+ ignoredComponents
+ );
+ }
+ });
+}
+
+function applyAttributes(
+ t: typeof Babel.types,
+ openingElement: Babel.NodePath,
+ componentName: string | null,
+ sourceFileName: string | undefined,
+ attributeNames: string[],
+ ignoredComponents: IgnoredComponent[]
+) {
+ const [componentAttributeName, elementAttributeName, sourceFileAttributeName] = attributeNames;
+
+ if (isReactFragment(t, openingElement)) {
+ return;
+ }
+ // e.g., Raw JSX text like the `A` in `a
`
+ if (!openingElement.node) {
+ return;
+ }
+
+ if (!openingElement.node.attributes) openingElement.node.attributes = [];
+ const elementName = getPathName(t, openingElement);
+
+ const isAnIgnoredComponent = ignoredComponents.some(
+ (ignoredComponent) =>
+ matchesIgnoreRule(ignoredComponent[0], sourceFileName) &&
+ matchesIgnoreRule(ignoredComponent[1], componentName) &&
+ matchesIgnoreRule(ignoredComponent[2], elementName)
+ );
+
+ // Add a stable attribute for the element name but only for non-DOM names
+ let isAnIgnoredElement = false;
+ if (
+ !isAnIgnoredComponent &&
+ !hasAttributeWithName(openingElement, componentAttributeName) &&
+ (componentAttributeName !== elementAttributeName || !componentName)
+ ) {
+ if (DEFAULT_IGNORED_ELEMENTS.includes(elementName)) {
+ isAnIgnoredElement = true;
+ } else {
+ // TODO: Is it possible to avoid this null check?
+ if (elementAttributeName) {
+ openingElement.node.attributes.push(
+ t.jSXAttribute(t.jSXIdentifier(elementAttributeName), t.stringLiteral(elementName))
+ );
+ }
+ }
+ }
+
+ // Add a stable attribute for the component name (absent for non-root elements)
+ if (
+ componentName &&
+ !isAnIgnoredComponent &&
+ !hasAttributeWithName(openingElement, componentAttributeName)
+ ) {
+ // TODO: Is it possible to avoid this null check?
+ if (componentAttributeName) {
+ openingElement.node.attributes.push(
+ t.jSXAttribute(t.jSXIdentifier(componentAttributeName), t.stringLiteral(componentName))
+ );
+ }
+ }
+
+ // Add a stable attribute for the source file name (absent for non-root elements)
+ if (
+ sourceFileName &&
+ !isAnIgnoredComponent &&
+ (componentName || isAnIgnoredElement === false) &&
+ !hasAttributeWithName(openingElement, sourceFileAttributeName)
+ ) {
+ // TODO: Is it possible to avoid this null check?
+ if (sourceFileAttributeName) {
+ openingElement.node.attributes.push(
+ t.jSXAttribute(t.jSXIdentifier(sourceFileAttributeName), t.stringLiteral(sourceFileName))
+ );
+ }
+ }
+}
+
+function sourceFileNameFromState(state: AnnotationPluginPass) {
+ const name = fullSourceFileNameFromState(state);
+ if (!name) {
+ return undefined;
+ }
+
+ if (name.indexOf("/") !== -1) {
+ return name.split("/").pop();
+ } else if (name.indexOf("\\") !== -1) {
+ return name.split("\\").pop();
+ } else {
+ return name;
+ }
+}
+
+function fullSourceFileNameFromState(state: AnnotationPluginPass): string | null {
+ // @ts-expect-error This type is incorrect in Babel, `sourceFileName` is the correct type
+ const name = state.file.opts.parserOpts?.sourceFileName as unknown;
+
+ if (typeof name === "string") {
+ return name;
+ }
+
+ return null;
+}
+
+function isKnownIncompatiblePluginFromState(state: AnnotationPluginPass) {
+ const fullSourceFileName = fullSourceFileNameFromState(state);
+
+ if (!fullSourceFileName) {
+ return false;
+ }
+
+ return KNOWN_INCOMPATIBLE_PLUGINS.some((pluginName) => {
+ if (
+ fullSourceFileName.includes(`/node_modules/${pluginName}/`) ||
+ fullSourceFileName.includes(`\\node_modules\\${pluginName}\\`)
+ ) {
+ return true;
+ }
+
+ return false;
+ });
+}
+
+function attributeNamesFromState(state: AnnotationPluginPass): [string, string, string] {
+ if (state.opts.native) {
+ return [nativeComponentName, nativeElementName, nativeSourceFileName];
+ }
+
+ return [webComponentName, webElementName, webSourceFileName];
+}
+
+function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath): boolean {
+ if (openingElement.isJSXFragment()) {
+ return true;
+ }
+
+ const elementName = getPathName(t, openingElement);
+
+ if (elementName === "Fragment" || elementName === "React.Fragment") {
+ return true;
+ }
+
+ // TODO: All these objects are typed as unknown, maybe an oversight in Babel types?
+ if (
+ openingElement.node &&
+ "name" in openingElement.node &&
+ openingElement.node.name &&
+ typeof openingElement.node.name === "object" &&
+ "type" in openingElement.node.name &&
+ openingElement.node.name.type === "JSXMemberExpression"
+ ) {
+ if (!("name" in openingElement.node)) {
+ return false;
+ }
+
+ const nodeName = openingElement.node.name;
+ if (typeof nodeName !== "object" || !nodeName) {
+ return false;
+ }
+
+ if ("object" in nodeName && "property" in nodeName) {
+ const nodeNameObject = nodeName.object;
+ const nodeNameProperty = nodeName.property;
+
+ if (typeof nodeNameObject !== "object" || typeof nodeNameProperty !== "object") {
+ return false;
+ }
+
+ if (!nodeNameObject || !nodeNameProperty) {
+ return false;
+ }
+
+ const objectName = "name" in nodeNameObject && nodeNameObject.name;
+ const propertyName = "name" in nodeNameProperty && nodeNameProperty.name;
+
+ if (objectName === "React" && propertyName === "Fragment") {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+function matchesIgnoreRule(rule: string, name: string | undefined | null) {
+ return rule === "*" || rule === name;
+}
+
+function hasAttributeWithName(
+ openingElement: Babel.NodePath,
+ name: string | undefined | null
+): boolean {
+ if (!name) {
+ return false;
+ }
+
+ return openingElement.node.attributes.some((node) => {
+ if (node.type === "JSXAttribute") {
+ return node.name.name === name;
+ }
+
+ return false;
+ });
+}
+
+function getPathName(t: typeof Babel.types, path: Babel.NodePath): string {
+ if (!path.node) return UNKNOWN_ELEMENT_NAME;
+ if (!("name" in path.node)) {
+ return UNKNOWN_ELEMENT_NAME;
+ }
+
+ const name = path.node.name;
+
+ if (typeof name === "string") {
+ return name;
+ }
+
+ if (t.isIdentifier(name) || t.isJSXIdentifier(name)) {
+ return name.name;
+ }
+
+ if (t.isJSXNamespacedName(name)) {
+ return name.name.name;
+ }
+
+ return UNKNOWN_ELEMENT_NAME;
+}
+
+const UNKNOWN_ELEMENT_NAME = "unknown";
diff --git a/packages/component-annotate-plugin/src/tsconfig.json b/packages/component-annotate-plugin/src/tsconfig.json
new file mode 100644
index 00000000..fd37e123
--- /dev/null
+++ b/packages/component-annotate-plugin/src/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@sentry-internal/sentry-bundler-plugin-tsconfig/base-config.json",
+ "include": ["./**/*", "../package.json"],
+ "compilerOptions": {
+ "esModuleInterop": true
+ }
+}
diff --git a/packages/component-annotate-plugin/test/__snapshots__/test-plugin.test.ts.snap b/packages/component-annotate-plugin/test/__snapshots__/test-plugin.test.ts.snap
new file mode 100644
index 00000000..a5c615e1
--- /dev/null
+++ b/packages/component-annotate-plugin/test/__snapshots__/test-plugin.test.ts.snap
@@ -0,0 +1,515 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`arrow snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(\\"div\\", {
+ \\"data-sentry-component\\": \\"componentName\\"
+ }, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+};
+export default componentName;"
+`;
+
+exports[`arrow-anonymous-fragment snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+const componentName = () => {
+ return (() => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\")))();
+};
+export default componentName;"
+`;
+
+exports[`arrow-anonymous-react-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => {
+ return (() => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\")))();
+};
+export default componentName;"
+`;
+
+exports[`arrow-anonymous-shorthand-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => {
+ return (() => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\")))();
+};
+export default componentName;"
+`;
+
+exports[`arrow-fragment snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+};
+export default componentName;"
+`;
+
+exports[`arrow-noreturn snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(\\"div\\", {
+ \\"data-sentry-component\\": \\"componentName\\"
+}, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-annotate-fragment snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", {
+ \\"data-sentry-component\\": \\"componentName\\",
+ \\"data-sentry-source-file\\": \\"filename-test.js\\"
+}, \\"Hello world\\"));
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-annotate-fragment-no-whitespace snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", {
+ \\"data-sentry-component\\": \\"componentName\\",
+ \\"data-sentry-source-file\\": \\"filename-test.js\\"
+}, \\"Hello world\\"), /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hola Sol\\"));
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-annotate-fragment-once snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", {
+ \\"data-sentry-component\\": \\"componentName\\",
+ \\"data-sentry-source-file\\": \\"filename-test.js\\"
+}, \\"Hello world\\"), /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hola Sol\\"));
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-annotate-react-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", {
+ \\"data-sentry-component\\": \\"componentName\\",
+ \\"data-sentry-source-file\\": \\"filename-test.js\\"
+}, \\"Hello world\\"));
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-annotate-shorthand-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", {
+ \\"data-sentry-component\\": \\"componentName\\",
+ \\"data-sentry-source-file\\": \\"filename-test.js\\"
+}, \\"Hello world\\"));
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-annotate-trivial-fragment snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, \\"Hello world\\");
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-fragment snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-react-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+export default componentName;"
+`;
+
+exports[`arrow-noreturn-shorthand-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+export default componentName;"
+`;
+
+exports[`arrow-react-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+};
+export default componentName;"
+`;
+
+exports[`arrow-shorthand-fragment snapshot matches 1`] = `
+"import React from 'react';
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+};
+export default componentName;"
+`;
+
+exports[`component snapshot matches 1`] = `
+"import React, { Component } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(\\"div\\", {
+ \\"data-sentry-component\\": \\"componentName\\"
+ }, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+ }
+}
+export default componentName;"
+`;
+
+exports[`component-annotate-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, \\"A\\");
+ }
+}
+export default componentName;"
+`;
+
+exports[`component-annotate-react-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", {
+ \\"data-sentry-component\\": \\"componentName\\",
+ \\"data-sentry-source-file\\": \\"filename-test.js\\"
+ }, \\"Hello world\\"));
+ }
+}
+export default componentName;"
+`;
+
+exports[`component-annotate-shorthand-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", {
+ \\"data-sentry-component\\": \\"componentName\\",
+ \\"data-sentry-source-file\\": \\"filename-test.js\\"
+ }, \\"Hello world\\"));
+ }
+}
+export default componentName;"
+`;
+
+exports[`component-fragment snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(Fragment, null, \\"A\\");
+ }
+}
+export default componentName;"
+`;
+
+exports[`component-fragment-native snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(Fragment, null, \\"A\\");
+ }
+}
+export default componentName;"
+`;
+
+exports[`component-react-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, \\"A\\");
+ }
+}
+export default componentName;"
+`;
+
+exports[`component-shorthand-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, \\"A\\");
+ }
+}
+export default componentName;"
+`;
+
+exports[`nonJSX snapshot matches 1`] = `
+"import React, { Component } from 'react';
+class TestClass extends Component {
+ test() {
+ return true;
+ }
+}
+export default TestClass;"
+`;
+
+exports[`option-attribute snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(\\"div\\", {
+ \\"data-sentry-component\\": \\"componentName\\"
+ }, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+};
+export default componentName;"
+`;
+
+exports[`option-format snapshot matches 1`] = `
+"import React, { Component } from 'react';
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(\\"div\\", {
+ \\"data-sentry-component\\": \\"componentName\\"
+ }, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+};
+export default componentName;"
+`;
+
+exports[`pure snapshot matches 1`] = `
+"import React from 'react';
+class PureComponentName extends React.PureComponent {
+ render() {
+ return /*#__PURE__*/React.createElement(\\"div\\", {
+ \\"data-sentry-component\\": \\"PureComponentName\\"
+ }, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+ }
+}
+export default PureComponentName;"
+`;
+
+exports[`pure-native snapshot matches 1`] = `
+"import React from 'react';
+class PureComponentName extends React.PureComponent {
+ render() {
+ return /*#__PURE__*/React.createElement(\\"div\\", {
+ dataSentryComponent: \\"PureComponentName\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+ }
+}
+export default PureComponentName;"
+`;
+
+exports[`pureComponent-fragment snapshot matches 1`] = `
+"import React, { Fragment } from 'react';
+class PureComponentName extends React.PureComponent {
+ render() {
+ return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+ }
+}
+export default PureComponentName;"
+`;
+
+exports[`pureComponent-react-fragment snapshot matches 1`] = `
+"import React from 'react';
+class PureComponentName extends React.PureComponent {
+ render() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+ }
+}
+export default PureComponentName;"
+`;
+
+exports[`pureComponent-shorthand-fragment snapshot matches 1`] = `
+"import React from 'react';
+class PureComponentName extends React.PureComponent {
+ render() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"Hello world\\"));
+ }
+}
+export default PureComponentName;"
+`;
+
+exports[`rawfunction snapshot matches 1`] = `
+"import React, { Component } from 'react';
+function SubComponent() {
+ return /*#__PURE__*/React.createElement(\\"div\\", {
+ \\"data-sentry-component\\": \\"SubComponent\\"
+ }, \\"Sub\\");
+}
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(\\"div\\", {
+ \\"data-sentry-component\\": \\"componentName\\"
+ }, /*#__PURE__*/React.createElement(SubComponent, {
+ \\"data-sentry-element\\": \\"SubComponent\\"
+ }));
+};
+export default componentName;"
+`;
+
+exports[`rawfunction-annotate-fragment snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+function SubComponent() {
+ return /*#__PURE__*/React.createElement(Fragment, null, \\"Sub\\");
+}
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(SubComponent, {
+ \\"data-sentry-element\\": \\"SubComponent\\",
+ \\"data-sentry-component\\": \\"componentName\\"
+ }));
+};
+export default componentName;"
+`;
+
+exports[`rawfunction-annotate-react-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+function SubComponent() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, \\"Sub\\");
+}
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, {
+ \\"data-sentry-element\\": \\"SubComponent\\",
+ \\"data-sentry-component\\": \\"componentName\\"
+ }));
+};
+export default componentName;"
+`;
+
+exports[`rawfunction-annotate-shorthand-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+function SubComponent() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, \\"Sub\\");
+}
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, {
+ \\"data-sentry-element\\": \\"SubComponent\\",
+ \\"data-sentry-component\\": \\"componentName\\"
+ }));
+};
+export default componentName;"
+`;
+
+exports[`rawfunction-fragment snapshot matches 1`] = `
+"import React, { Component, Fragment } from 'react';
+function SubComponent() {
+ return /*#__PURE__*/React.createElement(Fragment, null, \\"Sub\\");
+}
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(SubComponent, {
+ \\"data-sentry-element\\": \\"SubComponent\\"
+ }));
+};
+export default componentName;"
+`;
+
+exports[`rawfunction-react-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+function SubComponent() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, \\"Sub\\");
+}
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, {
+ \\"data-sentry-element\\": \\"SubComponent\\"
+ }));
+};
+export default componentName;"
+`;
+
+exports[`rawfunction-shorthand-fragment snapshot matches 1`] = `
+"import React, { Component } from 'react';
+function SubComponent() {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, \\"Sub\\");
+}
+const componentName = () => {
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SubComponent, {
+ \\"data-sentry-element\\": \\"SubComponent\\"
+ }));
+};
+export default componentName;"
+`;
+
+exports[`tags snapshot matches 1`] = `
+"import React, { Component } from 'react';
+import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+}
+class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ },
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text,
+ dataSentryElement: \\"TextInput\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+}
+export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container,
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"App\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, {
+ dataSentryElement: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(PizzaTranslator, {
+ dataSentryElement: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }));
+}
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+});"
+`;
+
+exports[`unknown-element snapshot matches 1`] = `
+"import React, { Component } from 'react';
+class componentName extends Component {
+ render() {
+ return /*#__PURE__*/React.createElement(\\"bogus\\", {
+ \\"data-sentry-element\\": \\"bogus\\",
+ \\"data-sentry-component\\": \\"componentName\\",
+ \\"data-sentry-source-file\\": \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(\\"h1\\", null, \\"A\\"));
+ }
+}
+export default componentName;"
+`;
diff --git a/packages/component-annotate-plugin/test/test-plugin.test.ts b/packages/component-annotate-plugin/test/test-plugin.test.ts
new file mode 100644
index 00000000..5a9ff11c
--- /dev/null
+++ b/packages/component-annotate-plugin/test/test-plugin.test.ts
@@ -0,0 +1,2238 @@
+/**
+ * MIT License
+ *
+ * Copyright (c) 2020 Engineering at FullStory
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+import { transform } from "@babel/core";
+import { componentNameAnnotatePlugin as plugin } from "../src/index";
+
+const BananasPizzaAppStandardInput = `import React, { Component } from 'react';
+import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+
+UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = "String";
+
+class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return ;
+ }
+}
+
+class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { text: '' };
+ }
+
+ render() {
+ return
+ this.setState({ text })} value={this.state.text} />
+
+ {this.state.text.split(' ').map(word => word && '🍕').join(' ')}
+
+ ;
+ }
+}
+
+export default function App() {
+ return
+ FullStory ReactNative testing app
+
+
+ ;
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+});`;
+
+const BananasStandardInput = `import React, { Component } from 'react';
+import { Image } from 'react-native';
+
+class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return ;
+ }
+}`;
+
+it("unknown-element snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+class componentName extends Component {
+ render() {
+ return A
;
+ }
+}
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("component-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+class componentName extends Component {
+ render() {
+ return A;
+ }
+}
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("component-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+class componentName extends Component {
+ render() {
+ return A;
+ }
+}
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("component-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+class componentName extends Component {
+ render() {
+ return <>A>;
+ }
+}
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("component-annotate-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+class componentName extends Component {
+ render() {
+ return <>A>;
+ }
+}
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("component-annotate-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+class componentName extends Component {
+ render() {
+ return
+ Hello world
+ ;
+ }
+}
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("component-annotate-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+class componentName extends Component {
+ render() {
+ return <>
+ Hello world
+ >;
+ }
+}
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+const componentName = () => (
+
+ Hello world
+
+);
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => (
+ <>
+ Hello world
+ >
+);
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => (
+
+ Hello world
+
+);
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-annotate-trivial-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+const componentName = () => (
+ Hello world
+);
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-annotate-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+const componentName = () => (
+
+ Hello world
+
+);
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-annotate-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => (
+
+ Hello world
+
+);
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-annotate-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => (
+ <>
+ Hello world
+ >
+);
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-annotate-fragment-once snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+const componentName = () => (
+
+ Hello world
+ Hola Sol
+
+);
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn-annotate-fragment-no-whitespace snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+const componentName = () => (
+ Hello world
Hola Sol
+);
+
+export default componentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => {
+ return
+
Hello world
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("option-attribute snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => {
+ return
+
Hello world
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("component snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+class componentName extends Component {
+ render() {
+ return
+
Hello world
+ ;
+ }
+}
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("rawfunction-annotate-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+function SubComponent() {
+ return Sub;
+}
+
+const componentName = () => {
+ return
+
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("rawfunction-annotate-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+function SubComponent() {
+ return Sub;
+}
+
+const componentName = () => {
+ return
+
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("rawfunction-annotate-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+function SubComponent() {
+ return <>Sub>;
+}
+
+const componentName = () => {
+ return <>
+
+ >;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { "annotate-fragments": true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("rawfunction-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+function SubComponent() {
+ return Sub;
+}
+
+const componentName = () => {
+ return
+
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("rawfunction-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+function SubComponent() {
+ return Sub;
+}
+
+const componentName = () => {
+ return
+
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("rawfunction-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+function SubComponent() {
+ return <>Sub>;
+}
+
+const componentName = () => {
+ return <>
+
+ >;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-noreturn snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => (
+
+
Hello world
+
+);
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("tags snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+
+UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = "String";
+
+class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return ;
+ }
+}
+
+class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { text: '' };
+ }
+
+ render() {
+ return
+ this.setState({ text })} value={this.state.text} />
+
+ {this.state.text.split(' ').map(word => word && '🍕').join(' ')}
+
+ ;
+ }
+}
+
+export default function App() {
+ return
+ FullStory ReactNative testing app
+
+
+ ;
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+});
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("option-format snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => {
+ return
+
Hello world
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("pureComponent-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Fragment } from 'react';
+
+class PureComponentName extends React.PureComponent {
+ render() {
+ return
+ Hello world
+ ;
+ }
+}
+
+export default PureComponentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("pureComponent-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React from 'react';
+
+class PureComponentName extends React.PureComponent {
+ render() {
+ return <>
+ Hello world
+ >;
+ }
+}
+
+export default PureComponentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("pureComponent-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React from 'react';
+
+class PureComponentName extends React.PureComponent {
+ render() {
+ return
+ Hello world
+ ;
+ }
+}
+
+export default PureComponentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("rawfunction snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+function SubComponent() {
+ return Sub
;
+}
+
+const componentName = () => {
+ return
+
+
;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+const componentName = () => {
+ return
+ Hello world
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React from 'react';
+
+const componentName = () => {
+ return <>
+ Hello world
+ >;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => {
+ return
+ Hello world
+ ;
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("nonJSX snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+class TestClass extends Component {
+ test() {
+ return true;
+ }
+}
+
+export default TestClass;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-anonymous-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+const componentName = () => {
+ return (() =>
+ Hello world
+ )();
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-anonymous-shorthand-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => {
+ return (() => <>
+ Hello world
+ >)();
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("arrow-anonymous-react-fragment snapshot matches", () => {
+ const result = transform(
+ `import React, { Component } from 'react';
+
+const componentName = () => {
+ return (() =>
+ Hello world
+ )();
+};
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("pure snapshot matches", () => {
+ const result = transform(
+ `import React from 'react';
+
+class PureComponentName extends React.PureComponent {
+ render() {
+ return
+
Hello world
+ ;
+ }
+}
+
+export default PureComponentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [plugin],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("component-fragment-native snapshot matches", () => {
+ const result = transform(
+ `import React, { Component, Fragment } from 'react';
+
+class componentName extends Component {
+ render() {
+ return A;
+ }
+}
+
+export default componentName;
+`,
+ {
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("pure-native snapshot matches", () => {
+ const result = transform(
+ `import React from 'react';
+
+class PureComponentName extends React.PureComponent {
+ render() {
+ return
+
Hello world
+ ;
+ }
+}
+
+export default PureComponentName;
+`,
+ {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true }]],
+ }
+ );
+ expect(result?.code).toMatchSnapshot();
+});
+
+it("Bananas ignore components dataSentrySourceFile=nomatch dataSentryComponent=nomatch dataSentryElement=nomatch snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "nomatch"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }"
+ `);
+});
+
+it("ignore components dataSentrySourceFile=* dataSentryComponent=nomatch dataSentryElement=nomatch snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["*", "nomatch", "nomatch"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }"
+ `);
+});
+
+it("Bananas ignore components dataSentrySourceFile=nomatch dataSentryComponent=* dataSentryElement=nomatch snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "*", "nomatch"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }"
+ `);
+});
+
+it("Bananas ignore components dataSentrySourceFile=nomatch dataSentryComponent=nomatch dataSentryElement=* snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "*"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }"
+ `);
+});
+
+it("Bananas ignore components dataSentrySourceFile=* dataSentryComponent=* dataSentryElement=nomatch snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "nomatch"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }"
+ `);
+});
+
+it("Bananas ignore components dataSentrySourceFile=* dataSentryComponent=nomatch dataSentryElement=* snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "nomatch"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }"
+ `);
+});
+
+it("Bananas ignore components dataSentrySourceFile=nomatch dataSentryComponent=* dataSentryElement=* snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "nomatch", "nomatch"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }"
+ `);
+});
+
+// This tests out matching only `dataSentryElement`, with * for the others
+it("Bananas ignore components dataSentrySourceFile=* dataSentryComponent=* dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["*", "*", "Image"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\"
+ });
+ }
+ }"
+ `);
+});
+
+// This tests out matching only `dataSentryElement` and `dataSentryComponent`, with * for `dataSentrySourceFile`
+it("Bananas ignore components dataSentrySourceFile=* dataSentryComponent=match dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["*", "Bananas", "Image"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\"
+ });
+ }
+ }"
+ `);
+});
+
+// This tests out matching on all 3 of our ignore list values
+it("Bananas ignore components dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [
+ [plugin, { native: true, ignoreComponents: [["filename-test.js", "Bananas", "Image"]] }],
+ ],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\"
+ });
+ }
+ }"
+ `);
+});
+
+// This tests out matching on all 3 of our ignore list values via *
+it("Bananas/Pizza/App ignore components dataSentrySourceFile=* dataSentryComponent=* dataSentryElement=* snapshot matches", () => {
+ const result = transform(BananasPizzaAppStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["*", "*", "*"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+ UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\"
+ });
+ }
+ }
+ class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ }
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ }
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+ }
+ export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ }
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, null), /*#__PURE__*/React.createElement(PizzaTranslator, null));
+ }
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+ });"
+ `);
+});
+
+// This tests out matching on all 3 of our ignore list values
+it("Bananas/Pizza/App ignore components dataSentrySourceFile=nomatch dataSentryComponent=* dataSentryElement=* snapshot matches", () => {
+ const result = transform(BananasPizzaAppStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true, ignoreComponents: [["nomatch.js", "*", "*"]] }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+ UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }
+ class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ },
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text,
+ dataSentryElement: \\"TextInput\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+ }
+ export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container,
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"App\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, {
+ dataSentryElement: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(PizzaTranslator, {
+ dataSentryElement: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }));
+ }
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+ });"
+ `);
+});
+
+it("Bananas/Pizza/App only Bananas dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasPizzaAppStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [
+ [
+ plugin,
+ {
+ native: true,
+ ignoreComponents: [
+ // Pizza
+ ["filename-test.js", "PizzaTranslator", "View"],
+ // App
+ ["filename-test.js", "App", "View"],
+ ],
+ },
+ ],
+ ],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+ UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }
+ class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ }
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text,
+ dataSentryElement: \\"TextInput\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+ }
+ export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, {
+ dataSentryElement: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(PizzaTranslator, {
+ dataSentryElement: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }));
+ }
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+ });"
+ `);
+});
+
+it("Bananas/Pizza/App only Pizza dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasPizzaAppStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [
+ [
+ plugin,
+ {
+ native: true,
+ ignoreComponents: [
+ // Bananas
+ ["filename-test.js", "Bananas", "Image"],
+ // App
+ ["filename-test.js", "App", "View"],
+ ],
+ },
+ ],
+ ],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+ UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\"
+ });
+ }
+ }
+ class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ },
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text,
+ dataSentryElement: \\"TextInput\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+ }
+ export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, {
+ dataSentryElement: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(PizzaTranslator, {
+ dataSentryElement: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }));
+ }
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+ });"
+ `);
+});
+
+it("Bananas/Pizza/App only App dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasPizzaAppStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [
+ [
+ plugin,
+ {
+ native: true,
+ ignoreComponents: [
+ // Bananas
+ ["filename-test.js", "Bananas", "Image"],
+ // Pizza
+ ["filename-test.js", "PizzaTranslator", "View"],
+ ],
+ },
+ ],
+ ],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+ UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\"
+ });
+ }
+ }
+ class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ }
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text,
+ dataSentryElement: \\"TextInput\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+ }
+ export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container,
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"App\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, {
+ dataSentryElement: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(PizzaTranslator, {
+ dataSentryElement: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }));
+ }
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+ });"
+ `);
+});
+
+it("Bananas/Pizza/App No Pizza Elements dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasPizzaAppStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [
+ [
+ plugin,
+ {
+ native: true,
+ ignoreComponents: [
+ // Pizza Element
+ ["filename-test.js", null, "PizzaTranslator"],
+ ],
+ },
+ ],
+ ],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+ UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }
+ class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ },
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text,
+ dataSentryElement: \\"TextInput\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+ }
+ export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container,
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"App\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, {
+ dataSentryElement: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(PizzaTranslator, null));
+ }
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+ });"
+ `);
+});
+
+it("Bananas/Pizza/App No Bananas Elements dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasPizzaAppStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [
+ [
+ plugin,
+ {
+ native: true,
+ ignoreComponents: [
+ // Bananas Element
+ ["filename-test.js", null, "Bananas"],
+ ],
+ },
+ ],
+ ],
+ });
+
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+ UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }
+ class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ },
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text,
+ dataSentryElement: \\"TextInput\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+ }
+ export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container,
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"App\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, null), /*#__PURE__*/React.createElement(PizzaTranslator, {
+ dataSentryElement: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }));
+ }
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+ });"
+ `);
+});
+
+it("Bananas/Pizza/App No Bananas/Pizza Elements dataSentrySourceFile=match dataSentryComponent=match dataSentryElement=match snapshot matches", () => {
+ const result = transform(BananasPizzaAppStandardInput, {
+ filename: "/filename-test.js",
+ configFile: false,
+ presets: ["@babel/preset-react"],
+ plugins: [
+ [
+ plugin,
+ {
+ native: true,
+ ignoreComponents: [
+ // Bananas Element
+ ["filename-test.js", null, "Bananas"],
+ // Pizza Element
+ ["filename-test.js", null, "PizzaTranslator"],
+ ],
+ },
+ ],
+ ],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native';
+ UIManager.getViewManagerConfig('RCTView').NativeProps.fsClass = \\"String\\";
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\",
+ dataSentryElement: \\"Image\\",
+ dataSentryComponent: \\"Bananas\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ });
+ }
+ }
+ class PizzaTranslator extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: ''
+ };
+ }
+ render() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: {
+ padding: 10
+ },
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"PizzaTranslator\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(TextInput, {
+ style: {
+ backgroundColor: '#000',
+ color: '#eee',
+ padding: 8
+ },
+ placeholder: \\"Type here to translate!\\" // not supported on iOS
+ ,
+ onChangeText: text => this.setState({
+ text
+ }),
+ value: this.state.text,
+ dataSentryElement: \\"TextInput\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }), /*#__PURE__*/React.createElement(Text, {
+ style: {
+ padding: 10,
+ fontSize: 42
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, this.state.text.split(' ').map(word => word && '🍕').join(' ')));
+ }
+ }
+ export default function App() {
+ return /*#__PURE__*/React.createElement(View, {
+ style: styles.container,
+ dataSentryElement: \\"View\\",
+ dataSentryComponent: \\"App\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, /*#__PURE__*/React.createElement(Text, {
+ style: {
+ color: '#eee'
+ },
+ dataSentryElement: \\"Text\\",
+ dataSentrySourceFile: \\"filename-test.js\\"
+ }, \\"FullStory ReactNative testing app\\"), /*#__PURE__*/React.createElement(Bananas, null), /*#__PURE__*/React.createElement(PizzaTranslator, null));
+ }
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'stretch',
+ backgroundColor: '#222',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }
+ });"
+ `);
+});
+
+it("Bananas incompatible plugin @react-navigation source snapshot matches", () => {
+ const result = transform(BananasStandardInput, {
+ filename: "test/node_modules/@react-navigation/core/filename-test.js",
+ presets: ["@babel/preset-react"],
+ plugins: [[plugin, { native: true }]],
+ });
+ expect(result?.code).toMatchInlineSnapshot(`
+ "import React, { Component } from 'react';
+ import { Image } from 'react-native';
+ class Bananas extends Component {
+ render() {
+ let pic = {
+ uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
+ };
+ return /*#__PURE__*/React.createElement(Image, {
+ source: pic,
+ style: {
+ width: 193,
+ height: 110,
+ marginTop: 10
+ },
+ fsClass: \\"test-class\\"
+ });
+ }
+ }"
+ `);
+});
diff --git a/packages/component-annotate-plugin/test/tsconfig.json b/packages/component-annotate-plugin/test/tsconfig.json
new file mode 100644
index 00000000..b73d9b53
--- /dev/null
+++ b/packages/component-annotate-plugin/test/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "../src/tsconfig.json",
+ "include": ["../src/**/*", "./**/*"],
+ "compilerOptions": {
+ "types": ["node", "jest"]
+ }
+}
diff --git a/packages/component-annotate-plugin/types.tsconfig.json b/packages/component-annotate-plugin/types.tsconfig.json
new file mode 100644
index 00000000..fb161bc4
--- /dev/null
+++ b/packages/component-annotate-plugin/types.tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./src/tsconfig.json",
+ "include": ["./src/**/*"],
+ "compilerOptions": {
+ "rootDir": "./src",
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "declarationDir": "./dist/types"
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 51d3cfb7..17504116 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -81,6 +81,13 @@
dependencies:
"@babel/types" "^7.18.6"
+"@babel/helper-annotate-as-pure@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
+ integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==
+ dependencies:
+ "@babel/types" "^7.22.5"
+
"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6":
version "7.21.5"
resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.21.5.tgz#817f73b6c59726ab39f6ba18c234268a519e5abb"
@@ -169,6 +176,13 @@
dependencies:
"@babel/types" "^7.21.4"
+"@babel/helper-module-imports@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
+ integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
+ dependencies:
+ "@babel/types" "^7.22.15"
+
"@babel/helper-module-transforms@^7.18.0", "@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.5":
version "7.21.5"
resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420"
@@ -195,6 +209,11 @@
resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56"
integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==
+"@babel/helper-plugin-utils@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295"
+ integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==
+
"@babel/helper-remap-async-to-generator@^7.18.9":
version "7.18.9"
resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519"
@@ -243,16 +262,31 @@
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd"
integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==
+"@babel/helper-string-parser@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
+ integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
+
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
+"@babel/helper-validator-identifier@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
+ integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
+
"@babel/helper-validator-option@^7.16.7", "@babel/helper-validator-option@^7.21.0":
version "7.21.0"
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180"
integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==
+"@babel/helper-validator-option@^7.22.15":
+ version "7.23.5"
+ resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
+ integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==
+
"@babel/helper-wrap-function@^7.18.9":
version "7.20.5"
resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3"
@@ -494,6 +528,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
+"@babel/plugin-syntax-jsx@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473"
+ integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.22.5"
+
"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
version "7.10.4"
resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
@@ -742,6 +783,39 @@
dependencies:
"@babel/helper-plugin-utils" "^7.18.6"
+"@babel/plugin-transform-react-display-name@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz#70529f034dd1e561045ad3c8152a267f0d7b6200"
+ integrity sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.22.5"
+
+"@babel/plugin-transform-react-jsx-development@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87"
+ integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==
+ dependencies:
+ "@babel/plugin-transform-react-jsx" "^7.22.5"
+
+"@babel/plugin-transform-react-jsx@^7.22.15", "@babel/plugin-transform-react-jsx@^7.22.5":
+ version "7.23.4"
+ resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz#393f99185110cea87184ea47bcb4a7b0c2e39312"
+ integrity sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.22.5"
+ "@babel/helper-module-imports" "^7.22.15"
+ "@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/plugin-syntax-jsx" "^7.23.3"
+ "@babel/types" "^7.23.4"
+
+"@babel/plugin-transform-react-pure-annotations@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz#fabedbdb8ee40edf5da96f3ecfc6958e3783b93c"
+ integrity sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.22.5"
+ "@babel/helper-plugin-utils" "^7.22.5"
+
"@babel/plugin-transform-regenerator@^7.18.0":
version "7.21.5"
resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz#576c62f9923f94bcb1c855adc53561fd7913724e"
@@ -910,6 +984,18 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
+"@babel/preset-react@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz#f73ca07e7590f977db07eb54dbe46538cc015709"
+ integrity sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.22.5"
+ "@babel/helper-validator-option" "^7.22.15"
+ "@babel/plugin-transform-react-display-name" "^7.23.3"
+ "@babel/plugin-transform-react-jsx" "^7.22.15"
+ "@babel/plugin-transform-react-jsx-development" "^7.22.5"
+ "@babel/plugin-transform-react-pure-annotations" "^7.23.3"
+
"@babel/preset-typescript@7.17.12":
version "7.17.12"
resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.17.12.tgz#40269e0a0084d56fc5731b6c40febe1c9a4a3e8c"
@@ -965,6 +1051,15 @@
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
+"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.4":
+ version "7.23.9"
+ resolved "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002"
+ integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==
+ dependencies:
+ "@babel/helper-string-parser" "^7.23.4"
+ "@babel/helper-validator-identifier" "^7.22.20"
+ to-fast-properties "^2.0.0"
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"