diff --git a/.gitignore b/.gitignore index 47a8abf5..1126d8eb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,5 @@ yarn-error.log packages/**/yarn.lock .DS_Store -packages/bundler-plugin-core/src/version.ts +packages/bundler-plugins/src/core/version.ts packages/integration-tests-next/fixtures/**/pnpm-lock.yaml diff --git a/.oxfmtrc.json b/.oxfmtrc.json index ba18e1d9..b34fe264 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -3,5 +3,9 @@ "printWidth": 100, "experimentalSortPackageJson": false, "trailingComma": "es5", - "ignorePatterns": ["packages/bundler-plugin-core/test/fixtures", ".nxcache"] + "ignorePatterns": [ + "packages/bundler-plugin-core/test/fixtures", + "packages/bundler-plugins/test/core/fixtures", + ".nxcache" + ] } diff --git a/package.json b/package.json index d9bac504..8d03e524 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "workspaces": [ "packages/babel-plugin-component-annotate", "packages/bundler-plugin-core", + "packages/bundler-plugins", "packages/dev-utils", "packages/esbuild-plugin", "packages/playground", diff --git a/packages/babel-plugin-component-annotate/package.json b/packages/babel-plugin-component-annotate/package.json index be6f555f..6506229f 100644 --- a/packages/babel-plugin-component-annotate/package.json +++ b/packages/babel-plugin-component-annotate/package.json @@ -45,15 +45,13 @@ "clean": "run-s clean:build", "clean:all": "run-p clean clean:deps", "clean:build": "premove ./dist *.tgz", - "clean:deps": "premove node_modules", - "test": "vitest run" + "clean:deps": "premove node_modules" + }, + "dependencies": { + "@sentry/bundler-plugins": "5.3.0" }, "devDependencies": { - "@babel/core": "7.18.5", - "@types/babel__core": "^7.20.5", - "@babel/preset-react": "^7.23.3", "@types/node": "^18.6.3", - "vitest": "^4.0.0", "premove": "^4.0.0", "rolldown": "^1.0.0" }, diff --git a/packages/babel-plugin-component-annotate/rollup.config.mjs b/packages/babel-plugin-component-annotate/rollup.config.mjs index 656ebdad..4e04614e 100644 --- a/packages/babel-plugin-component-annotate/rollup.config.mjs +++ b/packages/babel-plugin-component-annotate/rollup.config.mjs @@ -1,9 +1,11 @@ import packageJson from "./package.json" with { type: "json" }; +const deps = Object.keys(packageJson.dependencies ?? {}); + export default { platform: "node", input: ["src/index.ts"], - external: Object.keys(packageJson.dependencies ?? []), + external: (id) => deps.some((dep) => id === dep || id.startsWith(`${dep}/`)), output: [ { file: packageJson.module, diff --git a/packages/babel-plugin-component-annotate/src/index.ts b/packages/babel-plugin-component-annotate/src/index.ts index 13773f43..e3970985 100644 --- a/packages/babel-plugin-component-annotate/src/index.ts +++ b/packages/babel-plugin-component-annotate/src/index.ts @@ -1,890 +1,4 @@ -/* oxlint-disable max-lines */ -/** - * 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"; - -const SENTRY_LABEL_ATTRIBUTE = "sentry-label"; -const MAX_LABEL_LENGTH = 64; -const DEFAULT_TEXT_COMPONENT_NAMES = ["Text", "text"]; -const MAX_TEXT_SEARCH_DEPTH = 3; - -interface AutoInjectSentryLabelOpts { - textComponentNames?: string[]; -} - -interface AnnotationOpts { - native?: boolean; - "annotate-fragments"?: boolean; - ignoredComponents?: string[]; - /** @hidden */ - autoInjectSentryLabel?: boolean | AutoInjectSentryLabelOpts; -} - -interface FragmentContext { - fragmentAliases: Set; - reactNamespaceAliases: Set; -} - -interface AnnotationPluginPass extends PluginPass { - opts: AnnotationOpts; - sentryFragmentContext?: FragmentContext; -} - -type AnnotationPlugin = PluginObj; - -// Shared context object for all JSX processing functions -interface JSXProcessingContext { - /** Whether to annotate React fragments */ - annotateFragments: boolean; - /** Babel types object */ - t: typeof Babel.types; - /** Name of the React component */ - componentName: string; - /** Source file name (optional) */ - sourceFileName?: string; - /** Array of attribute names [component, element, sourceFile] */ - attributeNames: string[]; - /** Array of component names to ignore */ - ignoredComponents: string[]; - /** Fragment context for identifying React fragments */ - fragmentContext?: FragmentContext; - /** Whether to auto-inject sentry-label from static text children */ - autoInjectSentryLabel: boolean; - /** Component names whose JSXText children are considered text content */ - textComponentNames: string[]; -} - -export { experimentalComponentNameAnnotatePlugin } from "./experimental"; - -// We must export the plugin as default, otherwise the Babel loader will not be able to resolve it when configured using its string identifier -export default function componentNameAnnotatePlugin({ types: t }: typeof Babel): AnnotationPlugin { - return { - visitor: { - Program: { - enter(path, state) { - const fragmentContext = collectFragmentContext(path); - state.sentryFragmentContext = fragmentContext; - }, - }, - FunctionDeclaration(path, state) { - if (!path.node.id?.name) { - return; - } - if (isKnownIncompatiblePluginFromState(state)) { - return; - } - - const context = createJSXProcessingContext(state, t, path.node.id.name); - functionBodyPushAttributes(context, path); - }, - 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; - } - - const context = createJSXProcessingContext(state, t, parent.id.name); - functionBodyPushAttributes(context, path); - }, - 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?.traverse || isKnownIncompatiblePluginFromState(state)) { - return; - } - - const context = createJSXProcessingContext(state, t, name.node?.name || ""); - - render.traverse({ - ReturnStatement(returnStatement) { - const arg = returnStatement.get("argument"); - - if (!arg.isJSXElement() && !arg.isJSXFragment()) { - return; - } - - processJSX(context, arg); - }, - }); - }, - }, - }; -} - -/** - * Creates a JSX processing context from the plugin state - */ -function createJSXProcessingContext( - state: AnnotationPluginPass, - t: typeof Babel.types, - componentName: string -): JSXProcessingContext { - return { - annotateFragments: state.opts["annotate-fragments"] === true, - t, - componentName, - sourceFileName: sourceFileNameFromState(state), - attributeNames: attributeNamesFromState(state), - ignoredComponents: state.opts.ignoredComponents ?? [], - fragmentContext: state.sentryFragmentContext, - autoInjectSentryLabel: !!state.opts.autoInjectSentryLabel, - textComponentNames: - (state.opts.autoInjectSentryLabel && typeof state.opts.autoInjectSentryLabel === "object" - ? state.opts.autoInjectSentryLabel.textComponentNames - : undefined) ?? DEFAULT_TEXT_COMPONENT_NAMES, - }; -} - -/** - * Processes the body of a function to add Sentry tracking attributes to JSX elements. - * Handles various function body structures including direct JSX returns, conditional expressions, - * and nested JSX elements. - */ -function functionBodyPushAttributes( - context: JSXProcessingContext, - path: Babel.NodePath -): void { - 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; - } - - // Handle the case of a function body returning a ternary operation. - // `return (maybeTrue ? '' : ())` - if (arg.isConditionalExpression()) { - const consequent = arg.get("consequent"); - if (consequent.isJSXFragment() || consequent.isJSXElement()) { - processJSX(context, consequent); - } - const alternate = arg.get("alternate"); - if (alternate.isJSXFragment() || alternate.isJSXElement()) { - processJSX(context, alternate); - } - return; - } - - if (!arg.isJSXFragment() && !arg.isJSXElement()) { - return; - } - - jsxNode = arg; - } - - if (!jsxNode) { - return; - } - - processJSX(context, jsxNode); -} - -/** - * Recursively processes JSX elements to add Sentry tracking attributes. - * Handles both JSX elements and fragments, applying appropriate attributes - * based on configuration and component context. - */ -function processJSX( - context: JSXProcessingContext, - jsxNode: Babel.NodePath, - componentName?: string -): void { - if (!jsxNode) { - return; - } - - // Use provided componentName or fall back to context componentName - const currentComponentName = componentName ?? context.componentName; - const isRootElement = componentName === undefined; - - // 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( - context, - openingElement as Babel.NodePath, - currentComponentName - ); - }); - - 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 = context.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?.node) { - shouldSetComponentName = false; - processJSX(context, child, currentComponentName); - } else { - processJSX(context, child, ""); - } - }); - - if (isRootElement && context.autoInjectSentryLabel) { - maybeInjectSentryLabel(context, jsxNode); - } -} - -/** - * Applies Sentry tracking attributes to a JSX opening element. - * Adds component name, element name, and source file attributes while - * respecting ignore lists and fragment detection. - */ -function applyAttributes( - context: JSXProcessingContext, - openingElement: Babel.NodePath, - componentName: string -): void { - const { t, attributeNames, ignoredComponents, fragmentContext, sourceFileName } = context; - const [componentAttributeName, elementAttributeName, sourceFileAttributeName] = attributeNames; - - // e.g., Raw JSX text like the `A` in `

a

` - if (!openingElement.node) { - return; - } - - // Check if this is a React fragment - if so, skip attribute addition entirely - const isFragment = isReactFragment(t, openingElement, fragmentContext); - if (isFragment) { - return; - } - - if (!openingElement.node.attributes) openingElement.node.attributes = []; - const elementName = getPathName(t, openingElement); - - const isAnIgnoredComponent = ignoredComponents.some( - (ignoredComponent) => ignoredComponent === componentName || ignoredComponent === elementName - ); - - // Add a stable attribute for the element name but only for non-DOM names - let isAnIgnoredElement = false; - if (!isAnIgnoredComponent && !hasAttributeWithName(openingElement, elementAttributeName)) { - if (DEFAULT_IGNORED_ELEMENTS.includes(elementName)) { - isAnIgnoredElement = true; - } else { - // Always add element attribute for non-ignored elements - 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) - ) { - if (componentAttributeName) { - openingElement.node.attributes.push( - t.jSXAttribute(t.jSXIdentifier(componentAttributeName), t.stringLiteral(componentName)) - ); - } - } - - // Add a stable attribute for the source file name - // Updated condition: add source file for elements that have either: - // 1. A component name (root elements), OR - // 2. An element name that's not ignored (child elements) - if ( - sourceFileName && - !isAnIgnoredComponent && - (componentName || !isAnIgnoredElement) && - !hasAttributeWithName(openingElement, sourceFileAttributeName) - ) { - if (sourceFileAttributeName) { - openingElement.node.attributes.push( - t.jSXAttribute(t.jSXIdentifier(sourceFileAttributeName), t.stringLiteral(sourceFileName)) - ); - } - } -} - -function sourceFileNameFromState(state: AnnotationPluginPass): string | undefined { - 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): boolean { - 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 collectFragmentContext(programPath: Babel.NodePath): FragmentContext { - const fragmentAliases = new Set(); - const reactNamespaceAliases = new Set(["React"]); // Default React namespace - - programPath.traverse({ - ImportDeclaration(importPath) { - const source = importPath.node.source.value; - - // Handle React imports - if (source === "react" || source === "React") { - importPath.node.specifiers.forEach((spec) => { - if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") { - // Detect aliased React.Fragment imports (e.g., `Fragment as F`) - // so we can later identify as a fragment in JSX. - if (spec.imported.name === "Fragment") { - fragmentAliases.add(spec.local.name); - } - } else if ( - spec.type === "ImportDefaultSpecifier" || - spec.type === "ImportNamespaceSpecifier" - ) { - // import React from 'react' -> React OR - // import * as React from 'react' -> React - reactNamespaceAliases.add(spec.local.name); - } - }); - } - }, - - // Handle simple variable assignments only (avoid complex cases) - VariableDeclarator(varPath) { - if (varPath.node.init) { - const init = varPath.node.init; - - // Handle identifier assignments: const MyFragment = Fragment - if (varPath.node.id.type === "Identifier") { - // Handle: const MyFragment = Fragment (only if Fragment is a known alias) - if (init.type === "Identifier" && fragmentAliases.has(init.name)) { - fragmentAliases.add(varPath.node.id.name); - } - - // Handle: const MyFragment = React.Fragment (only for known React namespaces) - if ( - init.type === "MemberExpression" && - init.object.type === "Identifier" && - init.property.type === "Identifier" && - init.property.name === "Fragment" && - reactNamespaceAliases.has(init.object.name) - ) { - fragmentAliases.add(varPath.node.id.name); - } - } - - // Handle destructuring assignments: const { Fragment } = React - if (varPath.node.id.type === "ObjectPattern") { - if (init.type === "Identifier" && reactNamespaceAliases.has(init.name)) { - const properties = varPath.node.id.properties; - - for (const prop of properties) { - if ( - prop.type === "ObjectProperty" && - prop.key?.type === "Identifier" && - prop.value?.type === "Identifier" && - prop.key.name === "Fragment" - ) { - fragmentAliases.add(prop.value.name); - } - } - } - } - } - }, - }); - - return { fragmentAliases, reactNamespaceAliases }; -} - -function isReactFragment( - t: typeof Babel.types, - openingElement: Babel.NodePath, - context?: FragmentContext // Add this optional parameter -): boolean { - // Handle JSX fragments (<>) - if (openingElement.isJSXFragment()) { - return true; - } - - const elementName = getPathName(t, openingElement); - - // Direct fragment references - if (elementName === "Fragment" || elementName === "React.Fragment") { - return true; - } - - // TODO: All these objects are typed as unknown, maybe an oversight in Babel types? - - // Check if the element name is a known fragment alias - if (context && elementName && context.fragmentAliases.has(elementName)) { - return true; - } - - // Handle JSXMemberExpression - 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" - ) { - 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; - - // React.Fragment check - if (objectName === "React" && propertyName === "Fragment") { - return true; - } - - // Enhanced checks using context - if (context) { - // Check React.Fragment pattern with known React namespaces - if ( - context.reactNamespaceAliases.has(objectName as string) && - propertyName === "Fragment" - ) { - return true; - } - - // Check MyFragment.Fragment pattern - if (context.fragmentAliases.has(objectName as string) && propertyName === "Fragment") { - return true; - } - } - } - } - - return false; -} - -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; - } - - // Handle JSX member expressions like Tab.Group - if (t.isJSXMemberExpression(name)) { - const objectName = getJSXMemberExpressionObjectName(t, name.object); - const propertyName = name.property.name; - return `${objectName}.${propertyName}`; - } - - return UNKNOWN_ELEMENT_NAME; -} - -// Recursively handle nested member expressions (e.g. Components.UI.Header) -function getJSXMemberExpressionObjectName( - t: typeof Babel.types, - object: Babel.types.JSXMemberExpression | Babel.types.JSXIdentifier -): string { - if (t.isJSXIdentifier(object)) { - return object.name; - } - if (t.isJSXMemberExpression(object)) { - const objectName = getJSXMemberExpressionObjectName(t, object.object); - return `${objectName}.${object.property.name}`; - } - - return UNKNOWN_ELEMENT_NAME; -} - -/** - * Extracts static text content from JSX children, searching up to a depth limit. - * Collects text from JSXText nodes of the root element and from recognized - * text components (e.g. ). Non-text custom components are traversed - * but their own JSXText is not collected. - * - * Returns null when dynamic content is found anywhere in the subtree, - * signaling that the entire label should be skipped. - */ -function extractStaticTextFromChildren( - t: typeof Babel.types, - node: Babel.types.JSXElement | Babel.types.JSXFragment, - textComponentNames: string[], - depth: number, - isRoot: boolean -): string[] | null { - if (depth <= 0) { - return []; - } - - const texts: string[] = []; - - for (const child of node.children) { - if (t.isJSXText(child)) { - if (isRoot) { - const trimmed = child.value.replace(/\s+/g, " ").trim(); - if (trimmed) { - texts.push(trimmed); - } - } - } else if (t.isJSXElement(child)) { - const childName = getElementName(t, child.openingElement); - - if (textComponentNames.includes(childName)) { - const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); - if (innerTexts === null) { - return null; - } - texts.push(...innerTexts); - } else { - const result = extractStaticTextFromChildren( - t, - child, - textComponentNames, - depth - 1, - false - ); - if (result === null) { - return null; - } - texts.push(...result); - } - } else if (t.isJSXFragment(child)) { - const result = extractStaticTextFromChildren(t, child, textComponentNames, depth, isRoot); - if (result === null) { - return null; - } - texts.push(...result); - } else if (t.isJSXExpressionContainer(child)) { - if (!t.isJSXEmptyExpression(child.expression)) { - return null; - } - } else if (t.isJSXSpreadChild(child)) { - return null; - } - } - - return texts; -} - -/** - * Recursively extracts static text from within a recognized text component. - * Handles nested text components (e.g. Hello world) - * which is the standard React Native pattern for inline styling. - * - * Returns null when any dynamic content is found, signaling bail-out. - */ -function extractTextFromTextComponent( - t: typeof Babel.types, - node: Babel.types.JSXElement | Babel.types.JSXFragment, - textComponentNames: string[] -): string[] | null { - const texts: string[] = []; - - for (const child of node.children) { - if (t.isJSXText(child)) { - const trimmed = child.value.replace(/\s+/g, " ").trim(); - if (trimmed) { - texts.push(trimmed); - } - } else if (t.isJSXExpressionContainer(child)) { - if (!t.isJSXEmptyExpression(child.expression)) { - return null; - } - } else if (t.isJSXElement(child)) { - const childName = getElementName(t, child.openingElement); - if (textComponentNames.includes(childName)) { - const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); - if (innerTexts === null) { - return null; - } - texts.push(...innerTexts); - } else { - const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); - if (innerTexts === null) { - return null; - } - } - } else if (t.isJSXFragment(child)) { - const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); - if (innerTexts === null) { - return null; - } - texts.push(...innerTexts); - } else if (t.isJSXSpreadChild(child)) { - return null; - } - } - - return texts; -} - -function getElementName( - t: typeof Babel.types, - openingElement: Babel.types.JSXOpeningElement -): string { - const name = openingElement.name; - if (t.isJSXIdentifier(name)) { - return name.name; - } - if (t.isJSXMemberExpression(name)) { - return `${getJSXMemberExpressionObjectName(t, name.object)}.${name.property.name}`; - } - return ""; -} - -/** - * Injects a sentry-label attribute on the root JSX element of a component if - * static text content can be extracted from its children. - * - * When the root is a JSX fragment, the first JSXElement child is used as the - * target for both text extraction and attribute injection (since fragments - * cannot carry attributes). - */ -function maybeInjectSentryLabel(context: JSXProcessingContext, jsxNode: Babel.NodePath): void { - const { t, textComponentNames, ignoredComponents, componentName } = context; - const node = jsxNode.node; - - let targetElement: Babel.types.JSXElement; - - if (t.isJSXElement(node)) { - targetElement = node; - } else if (t.isJSXFragment(node)) { - const firstChild = node.children.find((c): c is Babel.types.JSXElement => t.isJSXElement(c)); - if (!firstChild) { - return; - } - targetElement = firstChild; - } else { - return; - } - - const targetElementName = getElementName(t, targetElement.openingElement); - - if ( - ignoredComponents.some((ignored) => ignored === componentName || ignored === targetElementName) - ) { - return; - } - - if ( - targetElement.openingElement.attributes.some( - (attr) => t.isJSXAttribute(attr) && attr.name.name === SENTRY_LABEL_ATTRIBUTE - ) - ) { - return; - } - - const texts = extractStaticTextFromChildren( - t, - targetElement, - textComponentNames, - MAX_TEXT_SEARCH_DEPTH, - true - ); - - if (texts === null) { - return; - } - - let label = texts.join(" ").replace(/\s+/g, " ").trim(); - - if (!label) { - return; - } - - if (label.length > MAX_LABEL_LENGTH) { - label = `${label.substring(0, MAX_LABEL_LENGTH - 3)}...`; - } - - targetElement.openingElement.attributes.push( - t.jSXAttribute(t.jSXIdentifier(SENTRY_LABEL_ATTRIBUTE), t.stringLiteral(label)) - ); -} - -const UNKNOWN_ELEMENT_NAME = "unknown"; +export { + default, + experimentalComponentNameAnnotatePlugin, +} from "@sentry/bundler-plugins/babel-plugin"; diff --git a/packages/bundler-plugin-core/package.json b/packages/bundler-plugin-core/package.json index f511ed1b..a71fa7d9 100644 --- a/packages/bundler-plugin-core/package.json +++ b/packages/bundler-plugin-core/package.json @@ -33,8 +33,7 @@ "module": "dist/esm/index.mjs", "types": "dist/types/index.d.ts", "scripts": { - "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", - "build": "premove ./out && run-p build:rollup build:types && run-s build:npm", + "build": "premove ./dist && run-p build:rollup build:types && run-s build:npm", "build:watch": "run-p build:rollup:watch build:types:watch", "build:rollup": "rolldown --config rollup.config.mjs", "build:rollup:watch": "rolldown --config rollup.config.mjs --watch --no-watch.clearScreen", @@ -47,26 +46,15 @@ "clean": "run-s clean:build", "clean:all": "run-p clean clean:deps", "clean:build": "premove ./dist *.tgz", - "clean:deps": "premove node_modules", - "pretest": "yarn prebuild", - "test": "vitest run" + "clean:deps": "premove node_modules" }, "dependencies": { - "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "5.3.0", - "@sentry/cli": "^2.58.6", - "dotenv": "^16.3.1", - "find-up": "^5.0.0", - "glob": "^13.0.6", - "magic-string": "~0.30.8" + "@sentry/bundler-plugins": "5.3.0" }, "devDependencies": { - "@sentry/core": "10.56.0", - "@sentry/types": "10.56.0", "@types/node": "^18.6.3", "premove": "^4.0.0", - "rolldown": "^1.0.0", - "vitest": "^4.0.0" + "rolldown": "^1.0.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/bundler-plugin-core/rollup.config.mjs b/packages/bundler-plugin-core/rollup.config.mjs index 98d8b5dd..4e04614e 100644 --- a/packages/bundler-plugin-core/rollup.config.mjs +++ b/packages/bundler-plugin-core/rollup.config.mjs @@ -1,9 +1,11 @@ import packageJson from "./package.json" with { type: "json" }; +const deps = Object.keys(packageJson.dependencies ?? {}); + export default { platform: "node", input: ["src/index.ts"], - external: Object.keys(packageJson.dependencies), + external: (id) => deps.some((dep) => id === dep || id.startsWith(`${dep}/`)), output: [ { file: packageJson.module, diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index a42ec5ab..cec33da9 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -1,145 +1 @@ -import { transformAsync } from "@babel/core"; -import componentNameAnnotatePlugin, { - experimentalComponentNameAnnotatePlugin, -} from "@sentry/babel-plugin-component-annotate"; -import SentryCli from "@sentry/cli"; -import { debug } from "@sentry/core"; -import * as fs from "fs"; -import { CodeInjection, containsOnlyImports, stripQueryAndHashFromPath } from "./utils"; - -/** - * Determines whether the Sentry CLI binary is in its expected location. - * This function is useful since `@sentry/cli` installs the binary via a post-install - * script and post-install scripts may not always run. E.g. with `npm i --ignore-scripts`. - */ -export function sentryCliBinaryExists(): boolean { - return fs.existsSync(SentryCli.getPath()); -} - -// We need to be careful not to inject the snippet before any `"use strict";`s. -// As an additional complication `"use strict";`s may come after any number of comments. -export const COMMENT_USE_STRICT_REGEX = - // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files. - /^(?:\s*|\/\*(?:.|\r|\n)*?\*\/|\/\/.*[\n\r])*(?:"[^"]*";|'[^']*';)?/; - -/** - * Checks if a file is a JavaScript file based on its extension. - * Handles query strings and hashes in the filename. - */ -export function isJsFile(fileName: string): boolean { - const cleanFileName = stripQueryAndHashFromPath(fileName); - return [".js", ".mjs", ".cjs"].some((ext) => cleanFileName.endsWith(ext)); -} - -/** - * Checks if a chunk should be skipped for code injection - * - * This is necessary to handle Vite's MPA (multi-page application) mode where - * HTML entry points create "facade" chunks that should not contain injected code. - * See: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829 - * - * However, in SPA mode, the main bundle also has an HTML facade but contains - * substantial application code. We should NOT skip injection for these bundles. - * - * @param code - The chunk's code content - * @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks - * @returns true if the chunk should be skipped - */ -export function shouldSkipCodeInjection( - code: string, - facadeModuleId: string | null | undefined -): boolean { - // Skip empty chunks - these are placeholder chunks that should be optimized away - if (code.trim().length === 0) { - return true; - } - - // For HTML facade chunks, only skip if they contain only import statements - if (facadeModuleId && stripQueryAndHashFromPath(facadeModuleId).endsWith(".html")) { - return containsOnlyImports(code); - } - - return false; -} - -export { globFiles } from "./glob"; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function createComponentNameAnnotateHooks( - ignoredComponents: string[], - injectIntoHtml: boolean -) { - type ParserPlugins = NonNullable< - NonNullable[1]>["parserOpts"] - >["plugins"]; - - return { - async transform(this: void, code: string, id: string) { - // id may contain query and hash which will trip up our file extension logic below - const idWithoutQueryAndHash = stripQueryAndHashFromPath(id); - - if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) { - return null; - } - - // We will only apply this plugin on jsx and tsx files - if (![".jsx", ".tsx"].some((ending) => idWithoutQueryAndHash.endsWith(ending))) { - return null; - } - - const parserPlugins: ParserPlugins = []; - if (idWithoutQueryAndHash.endsWith(".jsx")) { - parserPlugins.push("jsx"); - } else if (idWithoutQueryAndHash.endsWith(".tsx")) { - parserPlugins.push("jsx", "typescript"); - } - - const plugin = injectIntoHtml - ? experimentalComponentNameAnnotatePlugin - : componentNameAnnotatePlugin; - - try { - const result = await transformAsync(code, { - plugins: [[plugin, { ignoredComponents }]], - filename: id, - parserOpts: { - sourceType: "module", - allowAwaitOutsideFunction: true, - plugins: parserPlugins, - }, - generatorOpts: { - decoratorsBeforeExport: true, - }, - sourceMaps: true, - }); - - return { - code: result?.code ?? code, - map: result?.map, - }; - } catch (e) { - debug.error(`Failed to apply react annotate plugin`, e); - } - - return { code }; - }, - }; -} - -export function getDebugIdSnippet(debugId: string): CodeInjection { - return new CodeInjection( - `var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}");` - ); -} - -export type { Logger } from "./logger"; -export type { Options, SentrySDKBuildFlags } from "./types"; -export { - CodeInjection, - replaceBooleanFlagsInCode, - stringToUUID, - generateReleaseInjectorCode, - generateModuleMetadataInjectorCode, -} from "./utils"; -export { createSentryBuildPluginManager } from "./build-plugin-manager"; -export { createDebugIdUploadFunction } from "./debug-id-upload"; +export * from "@sentry/bundler-plugins/core"; diff --git a/packages/bundler-plugins/.gitignore b/packages/bundler-plugins/.gitignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/packages/bundler-plugins/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json new file mode 100644 index 00000000..44ad4d31 --- /dev/null +++ b/packages/bundler-plugins/package.json @@ -0,0 +1,127 @@ +{ + "name": "@sentry/bundler-plugins", + "version": "5.3.0", + "description": "Sentry Bundler Plugins", + "repository": "git://github.com/getsentry/sentry-javascript-bundler-plugins.git", + "homepage": "https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/bundler-plugins", + "author": "Sentry", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "sentry-release-injection-file.js", + "sentry-esbuild-debugid-injection-file.js" + ], + "exports": { + "./webpack": { + "types": "./dist/types/webpack/index.d.ts", + "import": "./dist/esm/webpack/index.mjs", + "require": "./dist/cjs/webpack/index.js" + }, + "./webpack5": { + "types": "./dist/types/webpack/webpack5.d.ts", + "import": "./dist/esm/webpack/webpack5.mjs", + "require": "./dist/cjs/webpack/webpack5.js" + }, + "./rollup": { + "types": "./dist/types/rollup/index.d.ts", + "import": "./dist/esm/rollup/index.mjs", + "require": "./dist/cjs/rollup/index.js" + }, + "./vite": { + "types": "./dist/types/vite/index.d.ts", + "import": "./dist/esm/vite/index.mjs", + "require": "./dist/cjs/vite/index.js" + }, + "./esbuild": { + "types": "./dist/types/esbuild/index.d.ts", + "import": "./dist/esm/esbuild/index.mjs", + "require": "./dist/cjs/esbuild/index.js" + }, + "./core": { + "types": "./dist/types/core/index.d.ts", + "import": "./dist/esm/core/index.mjs", + "require": "./dist/cjs/core/index.js" + }, + "./babel-plugin": { + "types": "./dist/types/babel-plugin/index.d.ts", + "import": "./dist/esm/babel-plugin/index.mjs", + "require": "./dist/cjs/babel-plugin/index.js" + }, + "./sentry-release-injection-file": { + "import": "./sentry-release-injection-file.js", + "require": "./sentry-release-injection-file.js" + }, + "./sentry-esbuild-debugid-injection-file": { + "import": "./sentry-esbuild-debugid-injection-file.js", + "require": "./sentry-esbuild-debugid-injection-file.js" + }, + "./webpack-loader": { + "require": "./dist/cjs/webpack/component-annotation-transform.js" + } + }, + "scripts": { + "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/core/version.ts", + "build": "premove ./dist && run-p build:rollup build:types && run-s build:npm", + "build:watch": "run-p build:rollup:watch build:types:watch", + "build:rollup": "rolldown --config rollup.config.mjs", + "build:rollup:watch": "rolldown --config rollup.config.mjs --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 ./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": "premove ./dist *.tgz", + "clean:deps": "premove node_modules", + "pretest": "yarn prebuild", + "test": "vitest run" + }, + "dependencies": { + "@babel/core": "^7.18.5", + "@sentry/cli": "^2.58.6", + "dotenv": "^16.3.1", + "find-up": "^5.0.0", + "glob": "^13.0.6", + "magic-string": "~0.30.8" + }, + "peerDependencies": { + "rollup": ">=3.2.0", + "webpack": ">=5.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "webpack": { + "optional": true + } + }, + "devDependencies": { + "@babel/preset-react": "^7.23.3", + "@sentry/core": "10.56.0", + "@sentry/types": "10.56.0", + "@sentry-internal/dev-utils": "5.3.0", + "@types/babel__core": "^7.20.5", + "@types/node": "^18.6.3", + "@types/webpack": "npm:@types/webpack@^4", + "premove": "^4.0.0", + "rolldown": "^1.0.0", + "vitest": "^4.0.0", + "webpack": "5.76.0" + }, + "volta": { + "extends": "../../package.json" + }, + "engines": { + "node": ">= 18" + }, + "sideEffects": [ + "./sentry-release-injection-file.js", + "./sentry-esbuild-debugid-injection-file.js" + ] +} diff --git a/packages/bundler-plugins/rollup.config.mjs b/packages/bundler-plugins/rollup.config.mjs new file mode 100644 index 00000000..8068953e --- /dev/null +++ b/packages/bundler-plugins/rollup.config.mjs @@ -0,0 +1,60 @@ +import packageJson from "./package.json" with { type: "json" }; +import modulePackage from "module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const srcDir = path.resolve(__dirname, "src"); + +const external = [ + ...Object.keys(packageJson.dependencies || {}), + ...modulePackage.builtinModules, + "webpack", + "rollup", + "vite", +]; + +export default { + platform: "node", + input: [ + "src/babel-plugin/index.ts", + "src/core/index.ts", + "src/rollup/index.ts", + "src/vite/index.ts", + "src/esbuild/index.ts", + "src/webpack/index.ts", + "src/webpack/webpack5.ts", + "src/webpack/component-annotation-transform.ts", + ], + external, + output: [ + { + dir: "./dist/esm", + format: "esm", + exports: "named", + sourcemap: true, + entryFileNames: (chunkInfo) => { + if (chunkInfo.facadeModuleId) { + const rel = path.relative(srcDir, chunkInfo.facadeModuleId); + return rel.replace(/\.ts$/, ".mjs"); + } + return "[name].mjs"; + }, + chunkFileNames: "_chunks/[name]-[hash].mjs", + }, + { + dir: "./dist/cjs", + format: "cjs", + exports: "named", + sourcemap: true, + entryFileNames: (chunkInfo) => { + if (chunkInfo.facadeModuleId) { + const rel = path.relative(srcDir, chunkInfo.facadeModuleId); + return rel.replace(/\.ts$/, ".js"); + } + return "[name].js"; + }, + chunkFileNames: "_chunks/[name]-[hash].js", + }, + ], +}; diff --git a/packages/bundler-plugins/sentry-esbuild-debugid-injection-file.js b/packages/bundler-plugins/sentry-esbuild-debugid-injection-file.js new file mode 100644 index 00000000..06ad5071 --- /dev/null +++ b/packages/bundler-plugins/sentry-esbuild-debugid-injection-file.js @@ -0,0 +1,20 @@ +try { + let globalObject = + "undefined" != typeof window + ? window + : "undefined" != typeof global + ? global + : "undefined" != typeof globalThis + ? global + : "undefined" != typeof self + ? self + : {}; + + let stack = new globalObject.Error().stack; + + if (stack) { + globalObject._sentryDebugIds = globalObject._sentryDebugIds || {}; + globalObject._sentryDebugIds[stack] = "__SENTRY_DEBUG_ID__"; + globalObject._sentryDebugIdIdentifier = "sentry-dbid-__SENTRY_DEBUG_ID__"; + } +} catch {} diff --git a/packages/bundler-plugins/sentry-release-injection-file.js b/packages/bundler-plugins/sentry-release-injection-file.js new file mode 100644 index 00000000..51de6958 --- /dev/null +++ b/packages/bundler-plugins/sentry-release-injection-file.js @@ -0,0 +1,4 @@ +// This const is used for nothing except to make this file identifiable via its content. +// We search for "_sentry_release_injection_file" in the plugin to determine for sure that the file we look at is the release injection file. + +// _sentry_release_injection_file diff --git a/packages/babel-plugin-component-annotate/src/constants.ts b/packages/bundler-plugins/src/babel-plugin/constants.ts similarity index 100% rename from packages/babel-plugin-component-annotate/src/constants.ts rename to packages/bundler-plugins/src/babel-plugin/constants.ts diff --git a/packages/babel-plugin-component-annotate/src/experimental.ts b/packages/bundler-plugins/src/babel-plugin/experimental.ts similarity index 100% rename from packages/babel-plugin-component-annotate/src/experimental.ts rename to packages/bundler-plugins/src/babel-plugin/experimental.ts diff --git a/packages/bundler-plugins/src/babel-plugin/index.ts b/packages/bundler-plugins/src/babel-plugin/index.ts new file mode 100644 index 00000000..13773f43 --- /dev/null +++ b/packages/bundler-plugins/src/babel-plugin/index.ts @@ -0,0 +1,890 @@ +/* oxlint-disable max-lines */ +/** + * 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"; + +const SENTRY_LABEL_ATTRIBUTE = "sentry-label"; +const MAX_LABEL_LENGTH = 64; +const DEFAULT_TEXT_COMPONENT_NAMES = ["Text", "text"]; +const MAX_TEXT_SEARCH_DEPTH = 3; + +interface AutoInjectSentryLabelOpts { + textComponentNames?: string[]; +} + +interface AnnotationOpts { + native?: boolean; + "annotate-fragments"?: boolean; + ignoredComponents?: string[]; + /** @hidden */ + autoInjectSentryLabel?: boolean | AutoInjectSentryLabelOpts; +} + +interface FragmentContext { + fragmentAliases: Set; + reactNamespaceAliases: Set; +} + +interface AnnotationPluginPass extends PluginPass { + opts: AnnotationOpts; + sentryFragmentContext?: FragmentContext; +} + +type AnnotationPlugin = PluginObj; + +// Shared context object for all JSX processing functions +interface JSXProcessingContext { + /** Whether to annotate React fragments */ + annotateFragments: boolean; + /** Babel types object */ + t: typeof Babel.types; + /** Name of the React component */ + componentName: string; + /** Source file name (optional) */ + sourceFileName?: string; + /** Array of attribute names [component, element, sourceFile] */ + attributeNames: string[]; + /** Array of component names to ignore */ + ignoredComponents: string[]; + /** Fragment context for identifying React fragments */ + fragmentContext?: FragmentContext; + /** Whether to auto-inject sentry-label from static text children */ + autoInjectSentryLabel: boolean; + /** Component names whose JSXText children are considered text content */ + textComponentNames: string[]; +} + +export { experimentalComponentNameAnnotatePlugin } from "./experimental"; + +// We must export the plugin as default, otherwise the Babel loader will not be able to resolve it when configured using its string identifier +export default function componentNameAnnotatePlugin({ types: t }: typeof Babel): AnnotationPlugin { + return { + visitor: { + Program: { + enter(path, state) { + const fragmentContext = collectFragmentContext(path); + state.sentryFragmentContext = fragmentContext; + }, + }, + FunctionDeclaration(path, state) { + if (!path.node.id?.name) { + return; + } + if (isKnownIncompatiblePluginFromState(state)) { + return; + } + + const context = createJSXProcessingContext(state, t, path.node.id.name); + functionBodyPushAttributes(context, path); + }, + 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; + } + + const context = createJSXProcessingContext(state, t, parent.id.name); + functionBodyPushAttributes(context, path); + }, + 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?.traverse || isKnownIncompatiblePluginFromState(state)) { + return; + } + + const context = createJSXProcessingContext(state, t, name.node?.name || ""); + + render.traverse({ + ReturnStatement(returnStatement) { + const arg = returnStatement.get("argument"); + + if (!arg.isJSXElement() && !arg.isJSXFragment()) { + return; + } + + processJSX(context, arg); + }, + }); + }, + }, + }; +} + +/** + * Creates a JSX processing context from the plugin state + */ +function createJSXProcessingContext( + state: AnnotationPluginPass, + t: typeof Babel.types, + componentName: string +): JSXProcessingContext { + return { + annotateFragments: state.opts["annotate-fragments"] === true, + t, + componentName, + sourceFileName: sourceFileNameFromState(state), + attributeNames: attributeNamesFromState(state), + ignoredComponents: state.opts.ignoredComponents ?? [], + fragmentContext: state.sentryFragmentContext, + autoInjectSentryLabel: !!state.opts.autoInjectSentryLabel, + textComponentNames: + (state.opts.autoInjectSentryLabel && typeof state.opts.autoInjectSentryLabel === "object" + ? state.opts.autoInjectSentryLabel.textComponentNames + : undefined) ?? DEFAULT_TEXT_COMPONENT_NAMES, + }; +} + +/** + * Processes the body of a function to add Sentry tracking attributes to JSX elements. + * Handles various function body structures including direct JSX returns, conditional expressions, + * and nested JSX elements. + */ +function functionBodyPushAttributes( + context: JSXProcessingContext, + path: Babel.NodePath +): void { + 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; + } + + // Handle the case of a function body returning a ternary operation. + // `return (maybeTrue ? '' : ())` + if (arg.isConditionalExpression()) { + const consequent = arg.get("consequent"); + if (consequent.isJSXFragment() || consequent.isJSXElement()) { + processJSX(context, consequent); + } + const alternate = arg.get("alternate"); + if (alternate.isJSXFragment() || alternate.isJSXElement()) { + processJSX(context, alternate); + } + return; + } + + if (!arg.isJSXFragment() && !arg.isJSXElement()) { + return; + } + + jsxNode = arg; + } + + if (!jsxNode) { + return; + } + + processJSX(context, jsxNode); +} + +/** + * Recursively processes JSX elements to add Sentry tracking attributes. + * Handles both JSX elements and fragments, applying appropriate attributes + * based on configuration and component context. + */ +function processJSX( + context: JSXProcessingContext, + jsxNode: Babel.NodePath, + componentName?: string +): void { + if (!jsxNode) { + return; + } + + // Use provided componentName or fall back to context componentName + const currentComponentName = componentName ?? context.componentName; + const isRootElement = componentName === undefined; + + // 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( + context, + openingElement as Babel.NodePath, + currentComponentName + ); + }); + + 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 = context.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?.node) { + shouldSetComponentName = false; + processJSX(context, child, currentComponentName); + } else { + processJSX(context, child, ""); + } + }); + + if (isRootElement && context.autoInjectSentryLabel) { + maybeInjectSentryLabel(context, jsxNode); + } +} + +/** + * Applies Sentry tracking attributes to a JSX opening element. + * Adds component name, element name, and source file attributes while + * respecting ignore lists and fragment detection. + */ +function applyAttributes( + context: JSXProcessingContext, + openingElement: Babel.NodePath, + componentName: string +): void { + const { t, attributeNames, ignoredComponents, fragmentContext, sourceFileName } = context; + const [componentAttributeName, elementAttributeName, sourceFileAttributeName] = attributeNames; + + // e.g., Raw JSX text like the `A` in `

a

` + if (!openingElement.node) { + return; + } + + // Check if this is a React fragment - if so, skip attribute addition entirely + const isFragment = isReactFragment(t, openingElement, fragmentContext); + if (isFragment) { + return; + } + + if (!openingElement.node.attributes) openingElement.node.attributes = []; + const elementName = getPathName(t, openingElement); + + const isAnIgnoredComponent = ignoredComponents.some( + (ignoredComponent) => ignoredComponent === componentName || ignoredComponent === elementName + ); + + // Add a stable attribute for the element name but only for non-DOM names + let isAnIgnoredElement = false; + if (!isAnIgnoredComponent && !hasAttributeWithName(openingElement, elementAttributeName)) { + if (DEFAULT_IGNORED_ELEMENTS.includes(elementName)) { + isAnIgnoredElement = true; + } else { + // Always add element attribute for non-ignored elements + 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) + ) { + if (componentAttributeName) { + openingElement.node.attributes.push( + t.jSXAttribute(t.jSXIdentifier(componentAttributeName), t.stringLiteral(componentName)) + ); + } + } + + // Add a stable attribute for the source file name + // Updated condition: add source file for elements that have either: + // 1. A component name (root elements), OR + // 2. An element name that's not ignored (child elements) + if ( + sourceFileName && + !isAnIgnoredComponent && + (componentName || !isAnIgnoredElement) && + !hasAttributeWithName(openingElement, sourceFileAttributeName) + ) { + if (sourceFileAttributeName) { + openingElement.node.attributes.push( + t.jSXAttribute(t.jSXIdentifier(sourceFileAttributeName), t.stringLiteral(sourceFileName)) + ); + } + } +} + +function sourceFileNameFromState(state: AnnotationPluginPass): string | undefined { + 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): boolean { + 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 collectFragmentContext(programPath: Babel.NodePath): FragmentContext { + const fragmentAliases = new Set(); + const reactNamespaceAliases = new Set(["React"]); // Default React namespace + + programPath.traverse({ + ImportDeclaration(importPath) { + const source = importPath.node.source.value; + + // Handle React imports + if (source === "react" || source === "React") { + importPath.node.specifiers.forEach((spec) => { + if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") { + // Detect aliased React.Fragment imports (e.g., `Fragment as F`) + // so we can later identify as a fragment in JSX. + if (spec.imported.name === "Fragment") { + fragmentAliases.add(spec.local.name); + } + } else if ( + spec.type === "ImportDefaultSpecifier" || + spec.type === "ImportNamespaceSpecifier" + ) { + // import React from 'react' -> React OR + // import * as React from 'react' -> React + reactNamespaceAliases.add(spec.local.name); + } + }); + } + }, + + // Handle simple variable assignments only (avoid complex cases) + VariableDeclarator(varPath) { + if (varPath.node.init) { + const init = varPath.node.init; + + // Handle identifier assignments: const MyFragment = Fragment + if (varPath.node.id.type === "Identifier") { + // Handle: const MyFragment = Fragment (only if Fragment is a known alias) + if (init.type === "Identifier" && fragmentAliases.has(init.name)) { + fragmentAliases.add(varPath.node.id.name); + } + + // Handle: const MyFragment = React.Fragment (only for known React namespaces) + if ( + init.type === "MemberExpression" && + init.object.type === "Identifier" && + init.property.type === "Identifier" && + init.property.name === "Fragment" && + reactNamespaceAliases.has(init.object.name) + ) { + fragmentAliases.add(varPath.node.id.name); + } + } + + // Handle destructuring assignments: const { Fragment } = React + if (varPath.node.id.type === "ObjectPattern") { + if (init.type === "Identifier" && reactNamespaceAliases.has(init.name)) { + const properties = varPath.node.id.properties; + + for (const prop of properties) { + if ( + prop.type === "ObjectProperty" && + prop.key?.type === "Identifier" && + prop.value?.type === "Identifier" && + prop.key.name === "Fragment" + ) { + fragmentAliases.add(prop.value.name); + } + } + } + } + } + }, + }); + + return { fragmentAliases, reactNamespaceAliases }; +} + +function isReactFragment( + t: typeof Babel.types, + openingElement: Babel.NodePath, + context?: FragmentContext // Add this optional parameter +): boolean { + // Handle JSX fragments (<>) + if (openingElement.isJSXFragment()) { + return true; + } + + const elementName = getPathName(t, openingElement); + + // Direct fragment references + if (elementName === "Fragment" || elementName === "React.Fragment") { + return true; + } + + // TODO: All these objects are typed as unknown, maybe an oversight in Babel types? + + // Check if the element name is a known fragment alias + if (context && elementName && context.fragmentAliases.has(elementName)) { + return true; + } + + // Handle JSXMemberExpression + 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" + ) { + 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; + + // React.Fragment check + if (objectName === "React" && propertyName === "Fragment") { + return true; + } + + // Enhanced checks using context + if (context) { + // Check React.Fragment pattern with known React namespaces + if ( + context.reactNamespaceAliases.has(objectName as string) && + propertyName === "Fragment" + ) { + return true; + } + + // Check MyFragment.Fragment pattern + if (context.fragmentAliases.has(objectName as string) && propertyName === "Fragment") { + return true; + } + } + } + } + + return false; +} + +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; + } + + // Handle JSX member expressions like Tab.Group + if (t.isJSXMemberExpression(name)) { + const objectName = getJSXMemberExpressionObjectName(t, name.object); + const propertyName = name.property.name; + return `${objectName}.${propertyName}`; + } + + return UNKNOWN_ELEMENT_NAME; +} + +// Recursively handle nested member expressions (e.g. Components.UI.Header) +function getJSXMemberExpressionObjectName( + t: typeof Babel.types, + object: Babel.types.JSXMemberExpression | Babel.types.JSXIdentifier +): string { + if (t.isJSXIdentifier(object)) { + return object.name; + } + if (t.isJSXMemberExpression(object)) { + const objectName = getJSXMemberExpressionObjectName(t, object.object); + return `${objectName}.${object.property.name}`; + } + + return UNKNOWN_ELEMENT_NAME; +} + +/** + * Extracts static text content from JSX children, searching up to a depth limit. + * Collects text from JSXText nodes of the root element and from recognized + * text components (e.g. ). Non-text custom components are traversed + * but their own JSXText is not collected. + * + * Returns null when dynamic content is found anywhere in the subtree, + * signaling that the entire label should be skipped. + */ +function extractStaticTextFromChildren( + t: typeof Babel.types, + node: Babel.types.JSXElement | Babel.types.JSXFragment, + textComponentNames: string[], + depth: number, + isRoot: boolean +): string[] | null { + if (depth <= 0) { + return []; + } + + const texts: string[] = []; + + for (const child of node.children) { + if (t.isJSXText(child)) { + if (isRoot) { + const trimmed = child.value.replace(/\s+/g, " ").trim(); + if (trimmed) { + texts.push(trimmed); + } + } + } else if (t.isJSXElement(child)) { + const childName = getElementName(t, child.openingElement); + + if (textComponentNames.includes(childName)) { + const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); + if (innerTexts === null) { + return null; + } + texts.push(...innerTexts); + } else { + const result = extractStaticTextFromChildren( + t, + child, + textComponentNames, + depth - 1, + false + ); + if (result === null) { + return null; + } + texts.push(...result); + } + } else if (t.isJSXFragment(child)) { + const result = extractStaticTextFromChildren(t, child, textComponentNames, depth, isRoot); + if (result === null) { + return null; + } + texts.push(...result); + } else if (t.isJSXExpressionContainer(child)) { + if (!t.isJSXEmptyExpression(child.expression)) { + return null; + } + } else if (t.isJSXSpreadChild(child)) { + return null; + } + } + + return texts; +} + +/** + * Recursively extracts static text from within a recognized text component. + * Handles nested text components (e.g. Hello world) + * which is the standard React Native pattern for inline styling. + * + * Returns null when any dynamic content is found, signaling bail-out. + */ +function extractTextFromTextComponent( + t: typeof Babel.types, + node: Babel.types.JSXElement | Babel.types.JSXFragment, + textComponentNames: string[] +): string[] | null { + const texts: string[] = []; + + for (const child of node.children) { + if (t.isJSXText(child)) { + const trimmed = child.value.replace(/\s+/g, " ").trim(); + if (trimmed) { + texts.push(trimmed); + } + } else if (t.isJSXExpressionContainer(child)) { + if (!t.isJSXEmptyExpression(child.expression)) { + return null; + } + } else if (t.isJSXElement(child)) { + const childName = getElementName(t, child.openingElement); + if (textComponentNames.includes(childName)) { + const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); + if (innerTexts === null) { + return null; + } + texts.push(...innerTexts); + } else { + const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); + if (innerTexts === null) { + return null; + } + } + } else if (t.isJSXFragment(child)) { + const innerTexts = extractTextFromTextComponent(t, child, textComponentNames); + if (innerTexts === null) { + return null; + } + texts.push(...innerTexts); + } else if (t.isJSXSpreadChild(child)) { + return null; + } + } + + return texts; +} + +function getElementName( + t: typeof Babel.types, + openingElement: Babel.types.JSXOpeningElement +): string { + const name = openingElement.name; + if (t.isJSXIdentifier(name)) { + return name.name; + } + if (t.isJSXMemberExpression(name)) { + return `${getJSXMemberExpressionObjectName(t, name.object)}.${name.property.name}`; + } + return ""; +} + +/** + * Injects a sentry-label attribute on the root JSX element of a component if + * static text content can be extracted from its children. + * + * When the root is a JSX fragment, the first JSXElement child is used as the + * target for both text extraction and attribute injection (since fragments + * cannot carry attributes). + */ +function maybeInjectSentryLabel(context: JSXProcessingContext, jsxNode: Babel.NodePath): void { + const { t, textComponentNames, ignoredComponents, componentName } = context; + const node = jsxNode.node; + + let targetElement: Babel.types.JSXElement; + + if (t.isJSXElement(node)) { + targetElement = node; + } else if (t.isJSXFragment(node)) { + const firstChild = node.children.find((c): c is Babel.types.JSXElement => t.isJSXElement(c)); + if (!firstChild) { + return; + } + targetElement = firstChild; + } else { + return; + } + + const targetElementName = getElementName(t, targetElement.openingElement); + + if ( + ignoredComponents.some((ignored) => ignored === componentName || ignored === targetElementName) + ) { + return; + } + + if ( + targetElement.openingElement.attributes.some( + (attr) => t.isJSXAttribute(attr) && attr.name.name === SENTRY_LABEL_ATTRIBUTE + ) + ) { + return; + } + + const texts = extractStaticTextFromChildren( + t, + targetElement, + textComponentNames, + MAX_TEXT_SEARCH_DEPTH, + true + ); + + if (texts === null) { + return; + } + + let label = texts.join(" ").replace(/\s+/g, " ").trim(); + + if (!label) { + return; + } + + if (label.length > MAX_LABEL_LENGTH) { + label = `${label.substring(0, MAX_LABEL_LENGTH - 3)}...`; + } + + targetElement.openingElement.attributes.push( + t.jSXAttribute(t.jSXIdentifier(SENTRY_LABEL_ATTRIBUTE), t.stringLiteral(label)) + ); +} + +const UNKNOWN_ELEMENT_NAME = "unknown"; diff --git a/packages/bundler-plugin-core/src/build-plugin-manager.ts b/packages/bundler-plugins/src/core/build-plugin-manager.ts similarity index 100% rename from packages/bundler-plugin-core/src/build-plugin-manager.ts rename to packages/bundler-plugins/src/core/build-plugin-manager.ts diff --git a/packages/bundler-plugin-core/src/debug-id-upload.ts b/packages/bundler-plugins/src/core/debug-id-upload.ts similarity index 100% rename from packages/bundler-plugin-core/src/debug-id-upload.ts rename to packages/bundler-plugins/src/core/debug-id-upload.ts diff --git a/packages/bundler-plugin-core/src/glob.ts b/packages/bundler-plugins/src/core/glob.ts similarity index 100% rename from packages/bundler-plugin-core/src/glob.ts rename to packages/bundler-plugins/src/core/glob.ts diff --git a/packages/bundler-plugins/src/core/index.ts b/packages/bundler-plugins/src/core/index.ts new file mode 100644 index 00000000..425c86f9 --- /dev/null +++ b/packages/bundler-plugins/src/core/index.ts @@ -0,0 +1,145 @@ +import { transformAsync } from "@babel/core"; +import componentNameAnnotatePlugin, { + experimentalComponentNameAnnotatePlugin, +} from "../babel-plugin"; +import SentryCli from "@sentry/cli"; +import { debug } from "@sentry/core"; +import * as fs from "fs"; +import { CodeInjection, containsOnlyImports, stripQueryAndHashFromPath } from "./utils"; + +/** + * Determines whether the Sentry CLI binary is in its expected location. + * This function is useful since `@sentry/cli` installs the binary via a post-install + * script and post-install scripts may not always run. E.g. with `npm i --ignore-scripts`. + */ +export function sentryCliBinaryExists(): boolean { + return fs.existsSync(SentryCli.getPath()); +} + +// We need to be careful not to inject the snippet before any `"use strict";`s. +// As an additional complication `"use strict";`s may come after any number of comments. +export const COMMENT_USE_STRICT_REGEX = + // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files. + /^(?:\s*|\/\*(?:.|\r|\n)*?\*\/|\/\/.*[\n\r])*(?:"[^"]*";|'[^']*';)?/; + +/** + * Checks if a file is a JavaScript file based on its extension. + * Handles query strings and hashes in the filename. + */ +export function isJsFile(fileName: string): boolean { + const cleanFileName = stripQueryAndHashFromPath(fileName); + return [".js", ".mjs", ".cjs"].some((ext) => cleanFileName.endsWith(ext)); +} + +/** + * Checks if a chunk should be skipped for code injection + * + * This is necessary to handle Vite's MPA (multi-page application) mode where + * HTML entry points create "facade" chunks that should not contain injected code. + * See: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829 + * + * However, in SPA mode, the main bundle also has an HTML facade but contains + * substantial application code. We should NOT skip injection for these bundles. + * + * @param code - The chunk's code content + * @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks + * @returns true if the chunk should be skipped + */ +export function shouldSkipCodeInjection( + code: string, + facadeModuleId: string | null | undefined +): boolean { + // Skip empty chunks - these are placeholder chunks that should be optimized away + if (code.trim().length === 0) { + return true; + } + + // For HTML facade chunks, only skip if they contain only import statements + if (facadeModuleId && stripQueryAndHashFromPath(facadeModuleId).endsWith(".html")) { + return containsOnlyImports(code); + } + + return false; +} + +export { globFiles } from "./glob"; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createComponentNameAnnotateHooks( + ignoredComponents: string[], + injectIntoHtml: boolean +) { + type ParserPlugins = NonNullable< + NonNullable[1]>["parserOpts"] + >["plugins"]; + + return { + async transform(this: void, code: string, id: string) { + // id may contain query and hash which will trip up our file extension logic below + const idWithoutQueryAndHash = stripQueryAndHashFromPath(id); + + if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) { + return null; + } + + // We will only apply this plugin on jsx and tsx files + if (![".jsx", ".tsx"].some((ending) => idWithoutQueryAndHash.endsWith(ending))) { + return null; + } + + const parserPlugins: ParserPlugins = []; + if (idWithoutQueryAndHash.endsWith(".jsx")) { + parserPlugins.push("jsx"); + } else if (idWithoutQueryAndHash.endsWith(".tsx")) { + parserPlugins.push("jsx", "typescript"); + } + + const plugin = injectIntoHtml + ? experimentalComponentNameAnnotatePlugin + : componentNameAnnotatePlugin; + + try { + const result = await transformAsync(code, { + plugins: [[plugin, { ignoredComponents }]], + filename: id, + parserOpts: { + sourceType: "module", + allowAwaitOutsideFunction: true, + plugins: parserPlugins, + }, + generatorOpts: { + decoratorsBeforeExport: true, + }, + sourceMaps: true, + }); + + return { + code: result?.code ?? code, + map: result?.map, + }; + } catch (e) { + debug.error(`Failed to apply react annotate plugin`, e); + } + + return { code }; + }, + }; +} + +export function getDebugIdSnippet(debugId: string): CodeInjection { + return new CodeInjection( + `var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}");` + ); +} + +export type { Logger } from "./logger"; +export type { Options, SentrySDKBuildFlags } from "./types"; +export { + CodeInjection, + replaceBooleanFlagsInCode, + stringToUUID, + generateReleaseInjectorCode, + generateModuleMetadataInjectorCode, +} from "./utils"; +export { createSentryBuildPluginManager } from "./build-plugin-manager"; +export { createDebugIdUploadFunction } from "./debug-id-upload"; diff --git a/packages/bundler-plugin-core/src/logger.ts b/packages/bundler-plugins/src/core/logger.ts similarity index 100% rename from packages/bundler-plugin-core/src/logger.ts rename to packages/bundler-plugins/src/core/logger.ts diff --git a/packages/bundler-plugin-core/src/options-mapping.ts b/packages/bundler-plugins/src/core/options-mapping.ts similarity index 100% rename from packages/bundler-plugin-core/src/options-mapping.ts rename to packages/bundler-plugins/src/core/options-mapping.ts diff --git a/packages/bundler-plugin-core/src/sentry/telemetry.ts b/packages/bundler-plugins/src/core/sentry/telemetry.ts similarity index 100% rename from packages/bundler-plugin-core/src/sentry/telemetry.ts rename to packages/bundler-plugins/src/core/sentry/telemetry.ts diff --git a/packages/bundler-plugin-core/src/sentry/transports.ts b/packages/bundler-plugins/src/core/sentry/transports.ts similarity index 100% rename from packages/bundler-plugin-core/src/sentry/transports.ts rename to packages/bundler-plugins/src/core/sentry/transports.ts diff --git a/packages/bundler-plugin-core/src/types.ts b/packages/bundler-plugins/src/core/types.ts similarity index 100% rename from packages/bundler-plugin-core/src/types.ts rename to packages/bundler-plugins/src/core/types.ts diff --git a/packages/bundler-plugin-core/src/utils.ts b/packages/bundler-plugins/src/core/utils.ts similarity index 100% rename from packages/bundler-plugin-core/src/utils.ts rename to packages/bundler-plugins/src/core/utils.ts diff --git a/packages/bundler-plugins/src/esbuild/index.ts b/packages/bundler-plugins/src/esbuild/index.ts new file mode 100644 index 00000000..57d5b2b6 --- /dev/null +++ b/packages/bundler-plugins/src/esbuild/index.ts @@ -0,0 +1,312 @@ +import type { Options } from "../core"; +import { + createSentryBuildPluginManager, + generateReleaseInjectorCode, + generateModuleMetadataInjectorCode, + getDebugIdSnippet, + createDebugIdUploadFunction, + CodeInjection, +} from "../core"; +import * as path from "node:path"; +import { createRequire } from "node:module"; +import { randomUUID } from "node:crypto"; + +interface EsbuildOnResolveArgs { + path: string; + kind: string; + importer?: string; + resolveDir: string; + pluginData?: unknown; +} + +interface EsbuildOnResolveResult { + path: string; + sideEffects?: boolean; + pluginName?: string; + namespace?: string; + suffix?: string; + pluginData?: unknown; +} + +interface EsbuildOnLoadArgs { + path: string; + pluginData?: unknown; +} + +interface EsbuildOnLoadResult { + loader: string; + pluginName: string; + contents: string; + resolveDir?: string; +} + +interface EsbuildOnEndArgs { + metafile?: { + outputs: Record; + }; +} + +interface EsbuildInitialOptions { + bundle?: boolean; + inject?: string[]; + metafile?: boolean; + define?: Record; +} + +interface EsbuildPluginBuild { + initialOptions: EsbuildInitialOptions; + onLoad: ( + options: { filter: RegExp; namespace?: string }, + callback: (args: EsbuildOnLoadArgs) => EsbuildOnLoadResult | null + ) => void; + onResolve: ( + options: { filter: RegExp }, + callback: (args: EsbuildOnResolveArgs) => EsbuildOnResolveResult | undefined + ) => void; + onEnd: (callback: (result: EsbuildOnEndArgs) => void | Promise) => void; +} + +function getEsbuildMajorVersion(): string | undefined { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - esbuild transpiles this for us + const req = createRequire(import.meta.url); + const esbuild = req("esbuild") as { version?: string }; + // esbuild hasn't released a v1 yet, so we'll return the minor version as the major version + return esbuild.version?.split(".")[1]; + } catch { + // do nothing, we'll just not report a version + } + + return undefined; +} + +const pluginName = "sentry-esbuild-plugin"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function sentryEsbuildPlugin(userOptions: Options = {}): any { + const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { + loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? `[${pluginName}]`, + buildTool: "esbuild", + buildToolMajorVersion: getEsbuildMajorVersion(), + }); + + const { + logger, + normalizedOptions: options, + bundleSizeOptimizationReplacementValues: replacementValues, + bundleMetadata, + createDependencyOnBuildArtifacts, + } = sentryBuildPluginManager; + + if (options.disable) { + return { + name: "sentry-esbuild-noop-plugin", + setup() { + // noop plugin + }, + }; + } + + if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) { + logger.warn( + "Running Sentry plugin from within a `node_modules` folder. Some features may not work." + ); + } + + const sourcemapsEnabled = options.sourcemaps?.disable !== true; + const staticInjectionCode = new CodeInjection(); + + if (!options.release.inject) { + logger.debug( + "Release injection disabled via `release.inject` option. Will not inject release." + ); + } else if (!options.release.name) { + logger.debug( + "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." + ); + } else { + staticInjectionCode.append( + generateReleaseInjectorCode({ + release: options.release.name, + injectBuildInformation: options._experiments.injectBuildInformation || false, + }) + ); + } + + if (Object.keys(bundleMetadata).length > 0) { + staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata)); + } + + // Component annotation warning + if (options.reactComponentAnnotation?.enabled) { + logger.warn( + "Component name annotation is not supported in esbuild. Please use a separate transform step or consider using a different bundler." + ); + } + + const transformReplace = Object.keys(replacementValues).length > 0; + + // Track entry points wrapped for debug ID injection + const debugIdWrappedPaths = new Set(); + + void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { + // Telemetry failures are acceptable + }); + + return { + name: pluginName, + setup({ initialOptions, onLoad, onResolve, onEnd }: EsbuildPluginBuild) { + // Release and/or metadata injection + if (!staticInjectionCode.isEmpty()) { + const virtualInjectionFilePath = path.resolve("_sentry-injection-stub"); + initialOptions.inject = initialOptions.inject || []; + initialOptions.inject.push(virtualInjectionFilePath); + + onResolve({ filter: /_sentry-injection-stub/ }, (args) => { + return { + path: args.path, + sideEffects: true, + pluginName, + }; + }); + + onLoad({ filter: /_sentry-injection-stub/ }, () => { + return { + loader: "js", + pluginName, + contents: staticInjectionCode.code(), + }; + }); + } + + // Bundle size optimizations + if (transformReplace) { + const replacementStringValues: Record = {}; + Object.entries(replacementValues).forEach(([key, value]) => { + replacementStringValues[key] = JSON.stringify(value); + }); + + initialOptions.define = { ...initialOptions.define, ...replacementStringValues }; + } + + // Debug ID injection - requires per-entry-point unique IDs + if (sourcemapsEnabled) { + // Clear state from previous builds (important for watch mode and test suites) + debugIdWrappedPaths.clear(); + + if (!initialOptions.bundle) { + logger.warn( + "The Sentry esbuild plugin only supports esbuild with `bundle: true` being set in the esbuild build options. Esbuild will probably crash now. Sorry about that. If you need to upload sourcemaps without `bundle: true`, it is recommended to use Sentry CLI instead: https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/cli/" + ); + } + + // Wrap entry points to inject debug IDs + onResolve({ filter: /.*/ }, (args) => { + if (args.kind !== "entry-point") { + return; + } + + // Skip injecting debug IDs into modules specified in the esbuild `inject` option + // since they're already part of the entry points + if (initialOptions.inject?.includes(args.path)) { + return; + } + + const resolvedPath = path.isAbsolute(args.path) + ? args.path + : path.join(args.resolveDir, args.path); + + // Skip injecting debug IDs into paths that have already been wrapped + if (debugIdWrappedPaths.has(resolvedPath)) { + return; + } + debugIdWrappedPaths.add(resolvedPath); + + return { + pluginName, + path: resolvedPath, + pluginData: { + isDebugIdProxy: true, + originalPath: args.path, + originalResolveDir: args.resolveDir, + }, + // We need to add a suffix here, otherwise esbuild will mark the entrypoint as resolved and won't traverse + // the module tree any further down past the proxy module because we're essentially creating a dependency + // loop back to the proxy module. + // By setting a suffix we're telling esbuild that the entrypoint and proxy module are two different things, + // making it re-resolve the entrypoint when it is imported from the proxy module. + // Super confusing? Yes. Works? Apparently... Let's see. + suffix: "?sentryDebugIdProxy=true", + }; + }); + + onLoad({ filter: /.*/ }, (args) => { + if (!(args.pluginData as { isDebugIdProxy?: boolean })?.isDebugIdProxy) { + return null; + } + + const originalPath = (args.pluginData as { originalPath: string }).originalPath; + const originalResolveDir = (args.pluginData as { originalResolveDir: string }) + .originalResolveDir; + + return { + loader: "js", + pluginName, + contents: ` + import "_sentry-debug-id-injection-stub"; + import * as OriginalModule from ${JSON.stringify(originalPath)}; + export default OriginalModule.default; + export * from ${JSON.stringify(originalPath)};`, + resolveDir: originalResolveDir, + }; + }); + + onResolve({ filter: /_sentry-debug-id-injection-stub/ }, (args) => { + return { + path: args.path, + sideEffects: true, + pluginName, + namespace: "sentry-debug-id-stub", + suffix: `?sentry-module-id=${randomUUID()}`, + }; + }); + + onLoad( + { filter: /_sentry-debug-id-injection-stub/, namespace: "sentry-debug-id-stub" }, + () => { + return { + loader: "js", + pluginName, + contents: getDebugIdSnippet(randomUUID()).code(), + }; + } + ); + } + + // Create release and optionally upload + const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); + const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); + + initialOptions.metafile = true; + onEnd(async (result) => { + try { + await sentryBuildPluginManager.createRelease(); + + if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") { + const buildArtifacts = result.metafile ? Object.keys(result.metafile.outputs) : []; + await upload(buildArtifacts); + } + } finally { + freeGlobalDependencyOnBuildArtifacts(); + await sentryBuildPluginManager.deleteArtifacts(); + } + }); + }, + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default sentryEsbuildPlugin; +export type { Options as SentryEsbuildPluginOptions } from "../core"; +export { sentryCliBinaryExists } from "../core"; diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts new file mode 100644 index 00000000..926e4c47 --- /dev/null +++ b/packages/bundler-plugins/src/rollup/index.ts @@ -0,0 +1,265 @@ +import type { Options } from "../core"; +import { + createSentryBuildPluginManager, + generateReleaseInjectorCode, + generateModuleMetadataInjectorCode, + isJsFile, + shouldSkipCodeInjection, + getDebugIdSnippet, + stringToUUID, + COMMENT_USE_STRICT_REGEX, + createDebugIdUploadFunction, + globFiles, + createComponentNameAnnotateHooks, + replaceBooleanFlagsInCode, + CodeInjection, +} from "../core"; +import type { SourceMap } from "magic-string"; +import MagicString from "magic-string"; +import type { TransformResult } from "rollup"; +import * as path from "node:path"; +import { createRequire } from "node:module"; + +function hasExistingDebugID(code: string): boolean { + // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI) + const chunkStartSnippet = code.slice(0, 6000); + const chunkEndSnippet = code.slice(-500); + + if ( + chunkStartSnippet.includes("_sentryDebugIdIdentifier") || + chunkEndSnippet.includes("//# debugId=") + ) { + return true; // Debug ID already present, skip injection + } + + return false; +} + +function getRollupMajorVersion(): string | undefined { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Rollup already transpiles this for us + const req = createRequire(import.meta.url); + const rollup = req("rollup") as { VERSION?: string }; + return rollup.VERSION?.split(".")[0]; + } catch { + // do nothing, we'll just not report a version + } + + return undefined; +} + +/** + * @ignore - this is the internal plugin factory function only used for the Vite plugin! + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function _rollupPluginInternal( + userOptions: Options = {}, + buildTool: "rollup" | "vite", + buildToolMajorVersion?: string +) { + const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { + loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? `[sentry-${buildTool}-plugin]`, + buildTool, + buildToolMajorVersion: buildToolMajorVersion || getRollupMajorVersion(), + }); + + const { + logger, + normalizedOptions: options, + bundleSizeOptimizationReplacementValues: replacementValues, + bundleMetadata, + createDependencyOnBuildArtifacts, + } = sentryBuildPluginManager; + + if (options.disable) { + return { + name: "sentry-noop-plugin", + }; + } + + if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) { + logger.warn( + "Running Sentry plugin from within a `node_modules` folder. Some features may not work." + ); + } + + const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); + const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); + const sourcemapsEnabled = options.sourcemaps?.disable !== true; + const staticInjectionCode = new CodeInjection(); + + if (!options.release.inject) { + logger.debug( + "Release injection disabled via `release.inject` option. Will not inject release." + ); + } else if (!options.release.name) { + logger.debug( + "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." + ); + } else { + staticInjectionCode.append( + generateReleaseInjectorCode({ + release: options.release.name, + injectBuildInformation: options._experiments.injectBuildInformation || false, + }) + ); + } + + if (Object.keys(bundleMetadata).length > 0) { + staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata)); + } + + const transformAnnotations = options.reactComponentAnnotation?.enabled + ? createComponentNameAnnotateHooks( + options.reactComponentAnnotation?.ignoredComponents || [], + !!options.reactComponentAnnotation?._experimentalInjectIntoHtml + ) + : undefined; + + const transformReplace = Object.keys(replacementValues).length > 0; + const shouldTransform = transformAnnotations || transformReplace; + + function buildStart(): void { + void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { + // Telemetry failures are acceptable + }); + } + + async function transform(code: string, id: string): Promise { + // Component annotations are only in user code and boolean flag replacements are + // only in Sentry code. If we successfully add annotations, we can return early. + + if (transformAnnotations?.transform) { + const result = await transformAnnotations.transform(code, id); + if (result) { + return result; + } + } + + if (transformReplace) { + return replaceBooleanFlagsInCode(code, replacementValues); + } + + return null; + } + + function renderChunk( + code: string, + chunk: { fileName: string; facadeModuleId?: string | null }, + _?: unknown, + meta?: { magicString?: MagicString } + ): { + code: string; + map?: SourceMap; + } | null { + if (!isJsFile(chunk.fileName)) { + return null; // returning null means not modifying the chunk at all + } + + // Skip empty chunks and HTML facade chunks (Vite MPA) + if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) { + return null; + } + + const injectCode = staticInjectionCode.clone(); + + if (sourcemapsEnabled && !hasExistingDebugID(code)) { + const debugId = stringToUUID(code); // generate a deterministic debug ID + injectCode.append(getDebugIdSnippet(debugId)); + } + + if (injectCode.isEmpty()) { + return null; + } + + const ms = meta?.magicString || new MagicString(code, { filename: chunk.fileName }); + const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; + + if (match) { + // Add injected code after any comments or "use strict" at the beginning of the bundle. + ms.appendLeft(match.length, injectCode.code()); + } else { + // ms.replace() doesn't work when there is an empty string match (which happens if + // there is neither, a comment, nor a "use strict" at the top of the chunk) so we + // need this special case here. + ms.prepend(injectCode.code()); + } + + // Rolldown can pass a native MagicString instance in meta.magicString + // https://rolldown.rs/in-depth/native-magic-string#usage-examples + if (ms?.constructor?.name === "BindingMagicString") { + // Rolldown docs say to return the magic string instance directly in this case + return { code: ms as unknown as string }; + } + + return { + code: ms.toString(), + map: ms.generateMap({ file: chunk.fileName, hires: "boundary" as unknown as undefined }), + }; + } + + async function writeBundle( + outputOptions: { dir?: string; file?: string }, + bundle: { [fileName: string]: unknown } + ): Promise { + try { + await sentryBuildPluginManager.createRelease(); + + if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") { + if (outputOptions.dir) { + const outputDir = outputOptions.dir; + const JS_AND_MAP_PATTERNS = [ + "/**/*.js", + "/**/*.mjs", + "/**/*.cjs", + "/**/*.js.map", + "/**/*.mjs.map", + "/**/*.cjs.map", + ].map((q) => `${q}?(\\?*)?(#*)`); // We want to allow query and hash strings at the end of files + const buildArtifacts = await globFiles(JS_AND_MAP_PATTERNS, { root: outputDir }); + await upload(buildArtifacts); + } else if (outputOptions.file) { + await upload([outputOptions.file]); + } else { + const buildArtifacts = Object.keys(bundle).map((asset) => + path.join(path.resolve(), asset) + ); + await upload(buildArtifacts); + } + } + } finally { + freeGlobalDependencyOnBuildArtifacts(); + await sentryBuildPluginManager.deleteArtifacts(); + } + } + + const name = `sentry-${buildTool}-plugin`; + + if (shouldTransform) { + return { + name, + buildStart, + transform, + renderChunk, + writeBundle, + }; + } + + return { + name, + buildStart, + renderChunk, + writeBundle, + }; +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any +export function sentryRollupPlugin(userOptions: Options = {}): any { + // We return an array here so we don't break backwards compatibility with what + // unplugin used to return + return [_rollupPluginInternal(userOptions, "rollup")]; +} + +export type { Options as SentryRollupPluginOptions } from "../core"; +export { sentryCliBinaryExists } from "../core"; diff --git a/packages/bundler-plugins/src/vite/index.ts b/packages/bundler-plugins/src/vite/index.ts new file mode 100644 index 00000000..6ca804ea --- /dev/null +++ b/packages/bundler-plugins/src/vite/index.ts @@ -0,0 +1,34 @@ +import type { SentryRollupPluginOptions } from "../rollup"; +import { _rollupPluginInternal } from "../rollup"; +import { createRequire } from "node:module"; + +interface SentryVitePlugin { + name: string; + enforce: "pre"; +} + +function getViteMajorVersion(): string | undefined { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Rollup already transpiles this for us + const req = createRequire(import.meta.url); + const vite = req("vite") as { version?: string }; + return vite.version?.split(".")[0]; + } catch { + // do nothing, we'll just not report a version + } + + return undefined; +} + +export const sentryVitePlugin = (options?: SentryRollupPluginOptions): SentryVitePlugin[] => { + return [ + { + enforce: "pre", + ..._rollupPluginInternal(options, "vite", getViteMajorVersion()), + }, + ]; +}; + +export type { Options as SentryVitePluginOptions } from "../core"; +export { sentryCliBinaryExists } from "../core"; diff --git a/packages/webpack-plugin/src/component-annotation-transform.ts b/packages/bundler-plugins/src/webpack/component-annotation-transform.ts similarity index 100% rename from packages/webpack-plugin/src/component-annotation-transform.ts rename to packages/bundler-plugins/src/webpack/component-annotation-transform.ts diff --git a/packages/bundler-plugins/src/webpack/index.ts b/packages/bundler-plugins/src/webpack/index.ts new file mode 100644 index 00000000..a63ffaa2 --- /dev/null +++ b/packages/bundler-plugins/src/webpack/index.ts @@ -0,0 +1,18 @@ +import type { SentryWebpackPluginOptions } from "./webpack4and5"; +import { sentryWebpackPluginFactory } from "./webpack4and5"; +import * as webpack4or5 from "webpack"; + +const BannerPlugin = webpack4or5?.BannerPlugin || webpack4or5?.default?.BannerPlugin; + +const DefinePlugin = webpack4or5?.DefinePlugin || webpack4or5?.default?.DefinePlugin; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = + sentryWebpackPluginFactory({ + BannerPlugin, + DefinePlugin, + }); + +export { sentryCliBinaryExists } from "../core"; + +export type { SentryWebpackPluginOptions }; diff --git a/packages/webpack-plugin/src/webpack4and5.ts b/packages/bundler-plugins/src/webpack/webpack4and5.ts similarity index 90% rename from packages/webpack-plugin/src/webpack4and5.ts rename to packages/bundler-plugins/src/webpack/webpack4and5.ts index a89aee6e..4a006dc8 100644 --- a/packages/webpack-plugin/src/webpack4and5.ts +++ b/packages/bundler-plugins/src/webpack/webpack4and5.ts @@ -1,4 +1,4 @@ -import type { Options } from "@sentry/bundler-plugin-core"; +import type { Options } from "../core/index"; import { createSentryBuildPluginManager, generateReleaseInjectorCode, @@ -8,22 +8,34 @@ import { CodeInjection, getDebugIdSnippet, createDebugIdUploadFunction, -} from "@sentry/bundler-plugin-core"; +} from "../core/index"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; import { randomUUID } from "node:crypto"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore Rollup transpiles import.meta for us for CJS -const dirname = path.dirname(fileURLToPath(import.meta.url)); - -const COMPONENT_ANNOTATION_LOADER = path.resolve( - dirname, - typeof __dirname !== "undefined" - ? "component-annotation-transform.js" // CJS - : "component-annotation-transform.mjs" // ESM -); +const _req = createRequire(import.meta.url); + +// Resolve the loader path via the package's own exports. +// webpack4and5.ts may end up in a shared chunk (_chunks/) whose import.meta.url +// does not point to the webpack/ directory where the transform file lives, so +// a path-relative lookup would fail. Using require.resolve on the package export +// always finds the correct installed file regardless of chunk placement. +let COMPONENT_ANNOTATION_LOADER: string; +try { + COMPONENT_ANNOTATION_LOADER = _req.resolve("@sentry/bundler-plugins/webpack-loader"); +} catch { + // Fallback for non-packaged environments (e.g., monorepo source runs without dist) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Rollup transpiles import.meta for us for CJS + const dirname = path.dirname(fileURLToPath(import.meta.url)); + COMPONENT_ANNOTATION_LOADER = path.resolve( + dirname, + typeof __dirname !== "undefined" + ? "component-annotation-transform.js" // CJS + : "component-annotation-transform.mjs" // ESM + ); +} // since webpack 5.1 compiler contains webpack module so plugins always use correct webpack version // https://github.com/webpack/webpack/commit/65eca2e529ce1d79b79200d4bdb1ce1b81141459 diff --git a/packages/bundler-plugins/src/webpack/webpack5.ts b/packages/bundler-plugins/src/webpack/webpack5.ts new file mode 100644 index 00000000..410b660c --- /dev/null +++ b/packages/bundler-plugins/src/webpack/webpack5.ts @@ -0,0 +1,12 @@ +import type { SentryWebpackPluginOptions } from "./webpack4and5"; +import { sentryWebpackPluginFactory } from "./webpack4and5"; + +const createSentryWebpackPlugin = sentryWebpackPluginFactory(); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = + createSentryWebpackPlugin; + +export { sentryCliBinaryExists } from "../core"; + +export type { SentryWebpackPluginOptions }; diff --git a/packages/babel-plugin-component-annotate/test/__snapshots__/test-plugin.test.ts.snap b/packages/bundler-plugins/test/babel-plugin/__snapshots__/test-plugin.test.ts.snap similarity index 100% rename from packages/babel-plugin-component-annotate/test/__snapshots__/test-plugin.test.ts.snap rename to packages/bundler-plugins/test/babel-plugin/__snapshots__/test-plugin.test.ts.snap diff --git a/packages/babel-plugin-component-annotate/test/experimental.test.ts b/packages/bundler-plugins/test/babel-plugin/experimental.test.ts similarity index 99% rename from packages/babel-plugin-component-annotate/test/experimental.test.ts rename to packages/bundler-plugins/test/babel-plugin/experimental.test.ts index 121582ff..29b04dee 100644 --- a/packages/babel-plugin-component-annotate/test/experimental.test.ts +++ b/packages/bundler-plugins/test/babel-plugin/experimental.test.ts @@ -25,7 +25,7 @@ import { describe, it, expect } from "vitest"; import { transform } from "@babel/core"; -import { experimentalComponentNameAnnotatePlugin as plugin } from "../src/index"; +import { experimentalComponentNameAnnotatePlugin as plugin } from "../../src/babel-plugin/index"; const BananasPizzaAppStandardInput = `import React, { Component } from 'react'; import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; diff --git a/packages/babel-plugin-component-annotate/test/sentry-label.test.ts b/packages/bundler-plugins/test/babel-plugin/sentry-label.test.ts similarity index 99% rename from packages/babel-plugin-component-annotate/test/sentry-label.test.ts rename to packages/bundler-plugins/test/babel-plugin/sentry-label.test.ts index e11f0af7..822eb95f 100644 --- a/packages/babel-plugin-component-annotate/test/sentry-label.test.ts +++ b/packages/bundler-plugins/test/babel-plugin/sentry-label.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import type { BabelFileResult } from "@babel/core"; import { transform } from "@babel/core"; -import plugin from "../src/index"; +import plugin from "../../src/babel-plugin/index"; function transformWith(code: string, opts: Record = {}): BabelFileResult | null { return transform(code, { diff --git a/packages/babel-plugin-component-annotate/test/test-plugin.test.ts b/packages/bundler-plugins/test/babel-plugin/test-plugin.test.ts similarity index 99% rename from packages/babel-plugin-component-annotate/test/test-plugin.test.ts rename to packages/bundler-plugins/test/babel-plugin/test-plugin.test.ts index cb9b61eb..84d97796 100644 --- a/packages/babel-plugin-component-annotate/test/test-plugin.test.ts +++ b/packages/bundler-plugins/test/babel-plugin/test-plugin.test.ts @@ -25,7 +25,7 @@ import { describe, it, expect } from "vitest"; import { transform } from "@babel/core"; -import plugin from "../src/index"; +import plugin from "../../src/babel-plugin/index"; const BananasPizzaAppStandardInput = `import React, { Component } from 'react'; import { StyleSheet, Text, TextInput, View, Image, UIManager } from 'react-native'; diff --git a/packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap b/packages/bundler-plugins/test/core/__snapshots__/utils.test.ts.snap similarity index 100% rename from packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap rename to packages/bundler-plugins/test/core/__snapshots__/utils.test.ts.snap diff --git a/packages/bundler-plugin-core/test/build-plugin-manager.test.ts b/packages/bundler-plugins/test/core/build-plugin-manager.test.ts similarity index 98% rename from packages/bundler-plugin-core/test/build-plugin-manager.test.ts rename to packages/bundler-plugins/test/core/build-plugin-manager.test.ts index 4dba3308..49717c60 100644 --- a/packages/bundler-plugin-core/test/build-plugin-manager.test.ts +++ b/packages/bundler-plugins/test/core/build-plugin-manager.test.ts @@ -1,10 +1,10 @@ import { createSentryBuildPluginManager, _resetDeployedReleasesForTesting, -} from "../src/build-plugin-manager"; +} from "../../src/core/build-plugin-manager"; import fs from "fs"; -import { globFiles } from "../src/glob"; -import { prepareBundleForDebugIdUpload } from "../src/debug-id-upload"; +import { globFiles } from "../../src/core/glob"; +import { prepareBundleForDebugIdUpload } from "../../src/core/debug-id-upload"; import type { MockedFunction } from "vitest"; import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; @@ -32,8 +32,8 @@ vi.mock("@sentry/cli", () => ({ }, })); -vi.mock("../src/sentry/telemetry", async () => ({ - ...(await vi.importActual("../src/sentry/telemetry")), +vi.mock("../../src/core/sentry/telemetry", async () => ({ + ...(await vi.importActual("../../src/core/sentry/telemetry")), safeFlushTelemetry: vi.fn(), })); @@ -42,8 +42,8 @@ vi.mock("@sentry/core", async () => ({ startSpan: vi.fn((options: unknown, callback: () => unknown) => callback()), })); -vi.mock("../src/glob"); -vi.mock("../src/debug-id-upload"); +vi.mock("../../src/core/glob"); +vi.mock("../../src/core/debug-id-upload"); const mockGlobFiles = globFiles as MockedFunction; const mockPrepareBundleForDebugIdUpload = diff --git a/packages/bundler-plugin-core/test/debug-id-upload.test.ts b/packages/bundler-plugins/test/core/debug-id-upload.test.ts similarity index 91% rename from packages/bundler-plugin-core/test/debug-id-upload.test.ts rename to packages/bundler-plugins/test/core/debug-id-upload.test.ts index 49ff8d4c..f4651fea 100644 --- a/packages/bundler-plugin-core/test/debug-id-upload.test.ts +++ b/packages/bundler-plugins/test/core/debug-id-upload.test.ts @@ -2,9 +2,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; -import { prepareBundleForDebugIdUpload } from "../src/debug-id-upload"; -import type { RewriteSourcesHook } from "../src/types"; -import type { Logger } from "../src"; +import { prepareBundleForDebugIdUpload } from "../../src/core/debug-id-upload"; +import type { RewriteSourcesHook } from "../../src/core/types"; +import type { Logger } from "../../src/core"; describe("prepareBundleForDebugIdUpload", () => { let tmpDir: string; diff --git a/packages/bundler-plugin-core/test/fixtures/deeply-nested-package/deeply/nested/index.js b/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/index.js similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/deeply-nested-package/deeply/nested/index.js rename to packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/index.js diff --git a/packages/bundler-plugin-core/test/fixtures/deeply-nested-package/deeply/nested/package.json b/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/package.json similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/deeply-nested-package/deeply/nested/package.json rename to packages/bundler-plugins/test/core/fixtures/deeply-nested-package/deeply/nested/package.json diff --git a/packages/bundler-plugin-core/test/fixtures/deeply-nested-package/package.json b/packages/bundler-plugins/test/core/fixtures/deeply-nested-package/package.json similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/deeply-nested-package/package.json rename to packages/bundler-plugins/test/core/fixtures/deeply-nested-package/package.json diff --git a/packages/bundler-plugin-core/test/fixtures/nested-error-package/deeply/nested/index.js b/packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/index.js similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/nested-error-package/deeply/nested/index.js rename to packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/index.js diff --git a/packages/bundler-plugin-core/test/fixtures/nested-error-package/deeply/nested/package.json b/packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/package.json similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/nested-error-package/deeply/nested/package.json rename to packages/bundler-plugins/test/core/fixtures/nested-error-package/deeply/nested/package.json diff --git a/packages/bundler-plugin-core/test/fixtures/nested-error-package/package.json b/packages/bundler-plugins/test/core/fixtures/nested-error-package/package.json similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/nested-error-package/package.json rename to packages/bundler-plugins/test/core/fixtures/nested-error-package/package.json diff --git a/packages/bundler-plugin-core/test/fixtures/nested-package/deeply/nested/index.js b/packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/index.js similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/nested-package/deeply/nested/index.js rename to packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/index.js diff --git a/packages/bundler-plugin-core/test/fixtures/nested-package/deeply/nested/package.json b/packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/package.json similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/nested-package/deeply/nested/package.json rename to packages/bundler-plugins/test/core/fixtures/nested-package/deeply/nested/package.json diff --git a/packages/bundler-plugin-core/test/fixtures/nested-package/package.json b/packages/bundler-plugins/test/core/fixtures/nested-package/package.json similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/nested-package/package.json rename to packages/bundler-plugins/test/core/fixtures/nested-package/package.json diff --git a/packages/bundler-plugin-core/test/fixtures/no-valid-package/deeply/nested/index.js b/packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/index.js similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/no-valid-package/deeply/nested/index.js rename to packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/index.js diff --git a/packages/bundler-plugin-core/test/fixtures/no-valid-package/deeply/nested/package.json b/packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/package.json similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/no-valid-package/deeply/nested/package.json rename to packages/bundler-plugins/test/core/fixtures/no-valid-package/deeply/nested/package.json diff --git a/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js b/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js rename to packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js diff --git a/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map b/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map rename to packages/bundler-plugins/test/core/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map diff --git a/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/bundles/index.js b/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/bundles/index.js similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/bundles/index.js rename to packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/bundles/index.js diff --git a/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map b/packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map similarity index 100% rename from packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map rename to packages/bundler-plugins/test/core/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map diff --git a/packages/bundler-plugin-core/test/glob.test.ts b/packages/bundler-plugins/test/core/glob.test.ts similarity index 99% rename from packages/bundler-plugin-core/test/glob.test.ts rename to packages/bundler-plugins/test/core/glob.test.ts index 808d3e91..9411a7bc 100644 --- a/packages/bundler-plugin-core/test/glob.test.ts +++ b/packages/bundler-plugins/test/core/glob.test.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; -import { globFiles } from "../src/glob"; +import { globFiles } from "../../src/core/glob"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; let tmpDir: string; diff --git a/packages/bundler-plugin-core/test/index.test.ts b/packages/bundler-plugins/test/core/index.test.ts similarity index 97% rename from packages/bundler-plugin-core/test/index.test.ts rename to packages/bundler-plugins/test/core/index.test.ts index fe4c03f0..5f9ee192 100644 --- a/packages/bundler-plugin-core/test/index.test.ts +++ b/packages/bundler-plugins/test/core/index.test.ts @@ -1,5 +1,5 @@ -import { getDebugIdSnippet } from "../src"; -import { containsOnlyImports } from "../src/utils"; +import { getDebugIdSnippet } from "../../src/core"; +import { containsOnlyImports } from "../../src/core/utils"; import { describe, it, expect } from "vitest"; describe("getDebugIdSnippet", () => { diff --git a/packages/bundler-plugin-core/test/option-mappings.test.ts b/packages/bundler-plugins/test/core/option-mappings.test.ts similarity index 98% rename from packages/bundler-plugin-core/test/option-mappings.test.ts rename to packages/bundler-plugins/test/core/option-mappings.test.ts index 7194adf3..c50d5187 100644 --- a/packages/bundler-plugin-core/test/option-mappings.test.ts +++ b/packages/bundler-plugins/test/core/option-mappings.test.ts @@ -1,6 +1,6 @@ -import type { Options } from "../src"; -import type { NormalizedOptions } from "../src/options-mapping"; -import { normalizeUserOptions, validateOptions } from "../src/options-mapping"; +import type { Options } from "../../src/core"; +import type { NormalizedOptions } from "../../src/core/options-mapping"; +import { normalizeUserOptions, validateOptions } from "../../src/core/options-mapping"; import { describe, it, test, expect, afterEach, vi, beforeEach } from "vitest"; describe("normalizeUserOptions()", () => { diff --git a/packages/bundler-plugin-core/test/sentry/logger.test.ts b/packages/bundler-plugins/test/core/sentry/logger.test.ts similarity index 98% rename from packages/bundler-plugin-core/test/sentry/logger.test.ts rename to packages/bundler-plugins/test/core/sentry/logger.test.ts index 1bb5d414..0e265569 100644 --- a/packages/bundler-plugin-core/test/sentry/logger.test.ts +++ b/packages/bundler-plugins/test/core/sentry/logger.test.ts @@ -1,4 +1,4 @@ -import { createLogger } from "../../src/logger"; +import { createLogger } from "../../../src/core/logger"; import { describe, it, expect, vi, afterEach } from "vitest"; describe("Logger", () => { diff --git a/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts b/packages/bundler-plugins/test/core/sentry/resolve-source-maps.test.ts similarity index 97% rename from packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts rename to packages/bundler-plugins/test/core/sentry/resolve-source-maps.test.ts index 8a9530f8..b7a5a2af 100644 --- a/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts +++ b/packages/bundler-plugins/test/core/sentry/resolve-source-maps.test.ts @@ -1,8 +1,8 @@ import * as path from "path"; import * as fs from "fs"; import * as url from "url"; -import { determineSourceMapPathFromBundle } from "../../src/debug-id-upload"; -import { createLogger } from "../../src/logger"; +import { determineSourceMapPathFromBundle } from "../../../src/core/debug-id-upload"; +import { createLogger } from "../../../src/core/logger"; import { describe, it, expect, vi } from "vitest"; const logger = createLogger({ prefix: "[resolve-source-maps-test]", silent: false, debug: false }); diff --git a/packages/bundler-plugin-core/test/sentry/telemetry.test.ts b/packages/bundler-plugins/test/core/sentry/telemetry.test.ts similarity index 94% rename from packages/bundler-plugin-core/test/sentry/telemetry.test.ts rename to packages/bundler-plugins/test/core/sentry/telemetry.test.ts index 97e63616..5474f6e5 100644 --- a/packages/bundler-plugin-core/test/sentry/telemetry.test.ts +++ b/packages/bundler-plugins/test/core/sentry/telemetry.test.ts @@ -1,7 +1,10 @@ import type { Scope } from "@sentry/core"; -import type { NormalizedOptions } from "../../src/options-mapping"; -import { normalizeUserOptions } from "../../src/options-mapping"; -import { allowedToSendTelemetry, setTelemetryDataOnScope } from "../../src/sentry/telemetry"; +import type { NormalizedOptions } from "../../../src/core/options-mapping"; +import { normalizeUserOptions } from "../../../src/core/options-mapping"; +import { + allowedToSendTelemetry, + setTelemetryDataOnScope, +} from "../../../src/core/sentry/telemetry"; import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; const { mockCliExecute } = vi.hoisted(() => ({ diff --git a/packages/bundler-plugin-core/test/utils.test.ts b/packages/bundler-plugins/test/core/utils.test.ts similarity index 98% rename from packages/bundler-plugin-core/test/utils.test.ts rename to packages/bundler-plugins/test/core/utils.test.ts index 11a32135..e229cccd 100644 --- a/packages/bundler-plugin-core/test/utils.test.ts +++ b/packages/bundler-plugins/test/core/utils.test.ts @@ -7,7 +7,7 @@ import { replaceBooleanFlagsInCode, serializeIgnoreOptions, stringToUUID, -} from "../src/utils"; +} from "../../src/core/utils"; import fs from "fs"; import { describe, it, expect, test, vi } from "vitest"; @@ -23,7 +23,7 @@ describe("getPackageJson", () => { test("it works for this package", () => { const packageJson = getPackageJson(); // eslint-disable-next-line @typescript-eslint/no-var-requires - const expected = require("../package.json") as PackageJson; + const expected = require("../../package.json") as PackageJson; expect(packageJson).toEqual(expected); }); diff --git a/packages/esbuild-plugin/test/public-api.test.ts b/packages/bundler-plugins/test/esbuild/public-api.test.ts similarity index 91% rename from packages/esbuild-plugin/test/public-api.test.ts rename to packages/bundler-plugins/test/esbuild/public-api.test.ts index a540fc0e..04a694e2 100644 --- a/packages/esbuild-plugin/test/public-api.test.ts +++ b/packages/bundler-plugins/test/esbuild/public-api.test.ts @@ -1,4 +1,4 @@ -import { sentryEsbuildPlugin } from "../src"; +import { sentryEsbuildPlugin } from "../../src/esbuild"; import type { Plugin } from "esbuild"; import { describe, it, expect, test } from "vitest"; diff --git a/packages/rollup-plugin/test/__snapshots__/public-api.test.ts.snap b/packages/bundler-plugins/test/rollup/__snapshots__/public-api.test.ts.snap similarity index 100% rename from packages/rollup-plugin/test/__snapshots__/public-api.test.ts.snap rename to packages/bundler-plugins/test/rollup/__snapshots__/public-api.test.ts.snap diff --git a/packages/rollup-plugin/test/public-api.test.ts b/packages/bundler-plugins/test/rollup/public-api.test.ts similarity index 99% rename from packages/rollup-plugin/test/public-api.test.ts rename to packages/bundler-plugins/test/rollup/public-api.test.ts index c0f6dc9c..3b4113c0 100644 --- a/packages/rollup-plugin/test/public-api.test.ts +++ b/packages/bundler-plugins/test/rollup/public-api.test.ts @@ -1,4 +1,4 @@ -import { sentryRollupPlugin } from "../src"; +import { sentryRollupPlugin } from "../../src/rollup"; import type { Plugin, SourceMap } from "rollup"; import { describe, it, expect, test, beforeEach, vi } from "vitest"; diff --git a/packages/bundler-plugins/test/tsconfig.json b/packages/bundler-plugins/test/tsconfig.json new file mode 100644 index 00000000..76d0c9cf --- /dev/null +++ b/packages/bundler-plugins/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../tsconfig.json", + "include": ["../src/**/*", "./**/*"], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/packages/vite-plugin/test/public-api.test.ts b/packages/bundler-plugins/test/vite/public-api.test.ts similarity index 95% rename from packages/vite-plugin/test/public-api.test.ts rename to packages/bundler-plugins/test/vite/public-api.test.ts index 285cb4ce..43e93467 100644 --- a/packages/vite-plugin/test/public-api.test.ts +++ b/packages/bundler-plugins/test/vite/public-api.test.ts @@ -1,4 +1,4 @@ -import { sentryVitePlugin } from "../src"; +import { sentryVitePlugin } from "../../src/vite"; import { describe, it, expect, test, beforeEach, vi } from "vitest"; test("Vite plugin should exist", () => { diff --git a/packages/webpack-plugin/test/public-api.test.ts b/packages/bundler-plugins/test/webpack/public-api.test.ts similarity index 91% rename from packages/webpack-plugin/test/public-api.test.ts rename to packages/bundler-plugins/test/webpack/public-api.test.ts index dbea8a52..098e48f2 100644 --- a/packages/webpack-plugin/test/public-api.test.ts +++ b/packages/bundler-plugins/test/webpack/public-api.test.ts @@ -1,5 +1,5 @@ import type { WebpackPluginInstance } from "webpack"; -import { sentryWebpackPlugin } from "../src"; +import { sentryWebpackPlugin } from "../../src/webpack"; import { describe, it, expect, test } from "vitest"; test("Webpack plugin should exist", () => { diff --git a/packages/webpack-plugin/test/webpack5.test.ts b/packages/bundler-plugins/test/webpack/webpack5.test.ts similarity index 91% rename from packages/webpack-plugin/test/webpack5.test.ts rename to packages/bundler-plugins/test/webpack/webpack5.test.ts index 0134d113..b6ab5d8d 100644 --- a/packages/webpack-plugin/test/webpack5.test.ts +++ b/packages/bundler-plugins/test/webpack/webpack5.test.ts @@ -1,5 +1,5 @@ import type { WebpackPluginInstance } from "webpack"; -import { sentryWebpackPlugin } from "../src/index"; +import { sentryWebpackPlugin } from "../../src/webpack/index"; import { describe, it, expect, test } from "vitest"; test("Webpack plugin should exist", () => { diff --git a/packages/bundler-plugins/tsconfig.json b/packages/bundler-plugins/tsconfig.json new file mode 100644 index 00000000..088fbb9b --- /dev/null +++ b/packages/bundler-plugins/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.json", + "include": ["./src/**/*.ts", "./package.json"], + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/packages/bundler-plugins/types.tsconfig.json b/packages/bundler-plugins/types.tsconfig.json new file mode 100644 index 00000000..e427dd96 --- /dev/null +++ b/packages/bundler-plugins/types.tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "include": ["./src/**/*"], + "compilerOptions": { + "rootDir": "./src", + "declaration": true, + "emitDeclarationOnly": true, + "declarationDir": "./dist/types" + } +} diff --git a/packages/esbuild-plugin/package.json b/packages/esbuild-plugin/package.json index 28783841..153bdb37 100644 --- a/packages/esbuild-plugin/package.json +++ b/packages/esbuild-plugin/package.json @@ -43,16 +43,14 @@ "clean:all": "run-p clean clean:deps", "clean:build": "premove ./dist *.tgz", "clean:deps": "premove node_modules", - "test": "vitest run", "prepack": "node ./prepack.mjs" }, "dependencies": { - "@sentry/bundler-plugin-core": "5.3.0" + "@sentry/bundler-plugins": "5.3.0" }, "devDependencies": { "@sentry-internal/dev-utils": "5.3.0", "@types/node": "^18.6.3", - "vitest": "^4.0.0", "premove": "^4.0.0", "rolldown": "^1.0.0" }, diff --git a/packages/esbuild-plugin/rollup.config.mjs b/packages/esbuild-plugin/rollup.config.mjs index 98d8b5dd..4e04614e 100644 --- a/packages/esbuild-plugin/rollup.config.mjs +++ b/packages/esbuild-plugin/rollup.config.mjs @@ -1,9 +1,11 @@ import packageJson from "./package.json" with { type: "json" }; +const deps = Object.keys(packageJson.dependencies ?? {}); + export default { platform: "node", input: ["src/index.ts"], - external: Object.keys(packageJson.dependencies), + external: (id) => deps.some((dep) => id === dep || id.startsWith(`${dep}/`)), output: [ { file: packageJson.module, diff --git a/packages/esbuild-plugin/src/index.ts b/packages/esbuild-plugin/src/index.ts index 8da22f9a..80e70f70 100644 --- a/packages/esbuild-plugin/src/index.ts +++ b/packages/esbuild-plugin/src/index.ts @@ -1,312 +1,2 @@ -import type { Options } from "@sentry/bundler-plugin-core"; -import { - createSentryBuildPluginManager, - generateReleaseInjectorCode, - generateModuleMetadataInjectorCode, - getDebugIdSnippet, - createDebugIdUploadFunction, - CodeInjection, -} from "@sentry/bundler-plugin-core"; -import * as path from "node:path"; -import { createRequire } from "node:module"; -import { randomUUID } from "node:crypto"; - -interface EsbuildOnResolveArgs { - path: string; - kind: string; - importer?: string; - resolveDir: string; - pluginData?: unknown; -} - -interface EsbuildOnResolveResult { - path: string; - sideEffects?: boolean; - pluginName?: string; - namespace?: string; - suffix?: string; - pluginData?: unknown; -} - -interface EsbuildOnLoadArgs { - path: string; - pluginData?: unknown; -} - -interface EsbuildOnLoadResult { - loader: string; - pluginName: string; - contents: string; - resolveDir?: string; -} - -interface EsbuildOnEndArgs { - metafile?: { - outputs: Record; - }; -} - -interface EsbuildInitialOptions { - bundle?: boolean; - inject?: string[]; - metafile?: boolean; - define?: Record; -} - -interface EsbuildPluginBuild { - initialOptions: EsbuildInitialOptions; - onLoad: ( - options: { filter: RegExp; namespace?: string }, - callback: (args: EsbuildOnLoadArgs) => EsbuildOnLoadResult | null - ) => void; - onResolve: ( - options: { filter: RegExp }, - callback: (args: EsbuildOnResolveArgs) => EsbuildOnResolveResult | undefined - ) => void; - onEnd: (callback: (result: EsbuildOnEndArgs) => void | Promise) => void; -} - -function getEsbuildMajorVersion(): string | undefined { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - esbuild transpiles this for us - const req = createRequire(import.meta.url); - const esbuild = req("esbuild") as { version?: string }; - // esbuild hasn't released a v1 yet, so we'll return the minor version as the major version - return esbuild.version?.split(".")[1]; - } catch { - // do nothing, we'll just not report a version - } - - return undefined; -} - -const pluginName = "sentry-esbuild-plugin"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function sentryEsbuildPlugin(userOptions: Options = {}): any { - const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { - loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? `[${pluginName}]`, - buildTool: "esbuild", - buildToolMajorVersion: getEsbuildMajorVersion(), - }); - - const { - logger, - normalizedOptions: options, - bundleSizeOptimizationReplacementValues: replacementValues, - bundleMetadata, - createDependencyOnBuildArtifacts, - } = sentryBuildPluginManager; - - if (options.disable) { - return { - name: "sentry-esbuild-noop-plugin", - setup() { - // noop plugin - }, - }; - } - - if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) { - logger.warn( - "Running Sentry plugin from within a `node_modules` folder. Some features may not work." - ); - } - - const sourcemapsEnabled = options.sourcemaps?.disable !== true; - const staticInjectionCode = new CodeInjection(); - - if (!options.release.inject) { - logger.debug( - "Release injection disabled via `release.inject` option. Will not inject release." - ); - } else if (!options.release.name) { - logger.debug( - "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." - ); - } else { - staticInjectionCode.append( - generateReleaseInjectorCode({ - release: options.release.name, - injectBuildInformation: options._experiments.injectBuildInformation || false, - }) - ); - } - - if (Object.keys(bundleMetadata).length > 0) { - staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata)); - } - - // Component annotation warning - if (options.reactComponentAnnotation?.enabled) { - logger.warn( - "Component name annotation is not supported in esbuild. Please use a separate transform step or consider using a different bundler." - ); - } - - const transformReplace = Object.keys(replacementValues).length > 0; - - // Track entry points wrapped for debug ID injection - const debugIdWrappedPaths = new Set(); - - void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { - // Telemetry failures are acceptable - }); - - return { - name: pluginName, - setup({ initialOptions, onLoad, onResolve, onEnd }: EsbuildPluginBuild) { - // Release and/or metadata injection - if (!staticInjectionCode.isEmpty()) { - const virtualInjectionFilePath = path.resolve("_sentry-injection-stub"); - initialOptions.inject = initialOptions.inject || []; - initialOptions.inject.push(virtualInjectionFilePath); - - onResolve({ filter: /_sentry-injection-stub/ }, (args) => { - return { - path: args.path, - sideEffects: true, - pluginName, - }; - }); - - onLoad({ filter: /_sentry-injection-stub/ }, () => { - return { - loader: "js", - pluginName, - contents: staticInjectionCode.code(), - }; - }); - } - - // Bundle size optimizations - if (transformReplace) { - const replacementStringValues: Record = {}; - Object.entries(replacementValues).forEach(([key, value]) => { - replacementStringValues[key] = JSON.stringify(value); - }); - - initialOptions.define = { ...initialOptions.define, ...replacementStringValues }; - } - - // Debug ID injection - requires per-entry-point unique IDs - if (sourcemapsEnabled) { - // Clear state from previous builds (important for watch mode and test suites) - debugIdWrappedPaths.clear(); - - if (!initialOptions.bundle) { - logger.warn( - "The Sentry esbuild plugin only supports esbuild with `bundle: true` being set in the esbuild build options. Esbuild will probably crash now. Sorry about that. If you need to upload sourcemaps without `bundle: true`, it is recommended to use Sentry CLI instead: https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/cli/" - ); - } - - // Wrap entry points to inject debug IDs - onResolve({ filter: /.*/ }, (args) => { - if (args.kind !== "entry-point") { - return; - } - - // Skip injecting debug IDs into modules specified in the esbuild `inject` option - // since they're already part of the entry points - if (initialOptions.inject?.includes(args.path)) { - return; - } - - const resolvedPath = path.isAbsolute(args.path) - ? args.path - : path.join(args.resolveDir, args.path); - - // Skip injecting debug IDs into paths that have already been wrapped - if (debugIdWrappedPaths.has(resolvedPath)) { - return; - } - debugIdWrappedPaths.add(resolvedPath); - - return { - pluginName, - path: resolvedPath, - pluginData: { - isDebugIdProxy: true, - originalPath: args.path, - originalResolveDir: args.resolveDir, - }, - // We need to add a suffix here, otherwise esbuild will mark the entrypoint as resolved and won't traverse - // the module tree any further down past the proxy module because we're essentially creating a dependency - // loop back to the proxy module. - // By setting a suffix we're telling esbuild that the entrypoint and proxy module are two different things, - // making it re-resolve the entrypoint when it is imported from the proxy module. - // Super confusing? Yes. Works? Apparently... Let's see. - suffix: "?sentryDebugIdProxy=true", - }; - }); - - onLoad({ filter: /.*/ }, (args) => { - if (!(args.pluginData as { isDebugIdProxy?: boolean })?.isDebugIdProxy) { - return null; - } - - const originalPath = (args.pluginData as { originalPath: string }).originalPath; - const originalResolveDir = (args.pluginData as { originalResolveDir: string }) - .originalResolveDir; - - return { - loader: "js", - pluginName, - contents: ` - import "_sentry-debug-id-injection-stub"; - import * as OriginalModule from ${JSON.stringify(originalPath)}; - export default OriginalModule.default; - export * from ${JSON.stringify(originalPath)};`, - resolveDir: originalResolveDir, - }; - }); - - onResolve({ filter: /_sentry-debug-id-injection-stub/ }, (args) => { - return { - path: args.path, - sideEffects: true, - pluginName, - namespace: "sentry-debug-id-stub", - suffix: `?sentry-module-id=${randomUUID()}`, - }; - }); - - onLoad( - { filter: /_sentry-debug-id-injection-stub/, namespace: "sentry-debug-id-stub" }, - () => { - return { - loader: "js", - pluginName, - contents: getDebugIdSnippet(randomUUID()).code(), - }; - } - ); - } - - // Create release and optionally upload - const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); - const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); - - initialOptions.metafile = true; - onEnd(async (result) => { - try { - await sentryBuildPluginManager.createRelease(); - - if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") { - const buildArtifacts = result.metafile ? Object.keys(result.metafile.outputs) : []; - await upload(buildArtifacts); - } - } finally { - freeGlobalDependencyOnBuildArtifacts(); - await sentryBuildPluginManager.deleteArtifacts(); - } - }); - }, - }; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default sentryEsbuildPlugin; -export type { Options as SentryEsbuildPluginOptions } from "@sentry/bundler-plugin-core"; -export { sentryCliBinaryExists } from "@sentry/bundler-plugin-core"; +export * from "@sentry/bundler-plugins/esbuild"; +export { default } from "@sentry/bundler-plugins/esbuild"; diff --git a/packages/integration-tests-next/fixtures/esbuild/package.json b/packages/integration-tests-next/fixtures/esbuild/package.json index ae9341e2..b6f09666 100644 --- a/packages/integration-tests-next/fixtures/esbuild/package.json +++ b/packages/integration-tests-next/fixtures/esbuild/package.json @@ -9,6 +9,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/esbuild-plugin": "file:../../../esbuild-plugin/sentry-esbuild-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/rolldown/package.json b/packages/integration-tests-next/fixtures/rolldown/package.json index 1aec87ac..43b95341 100644 --- a/packages/integration-tests-next/fixtures/rolldown/package.json +++ b/packages/integration-tests-next/fixtures/rolldown/package.json @@ -10,6 +10,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/rollup3/package.json b/packages/integration-tests-next/fixtures/rollup3/package.json index 53cd21e6..aa29336e 100644 --- a/packages/integration-tests-next/fixtures/rollup3/package.json +++ b/packages/integration-tests-next/fixtures/rollup3/package.json @@ -12,6 +12,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/rollup4/package.json b/packages/integration-tests-next/fixtures/rollup4/package.json index 944d4479..5abcc765 100644 --- a/packages/integration-tests-next/fixtures/rollup4/package.json +++ b/packages/integration-tests-next/fixtures/rollup4/package.json @@ -12,6 +12,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/integration-tests-next/fixtures/vite4/package.json b/packages/integration-tests-next/fixtures/vite4/package.json index 0a10b4e9..cd6089fc 100644 --- a/packages/integration-tests-next/fixtures/vite4/package.json +++ b/packages/integration-tests-next/fixtures/vite4/package.json @@ -11,6 +11,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/vite-plugin": "file:../../../vite-plugin/sentry-vite-plugin-5.3.0.tgz", diff --git a/packages/integration-tests-next/fixtures/vite6/package.json b/packages/integration-tests-next/fixtures/vite6/package.json index 038495ef..adf5500e 100644 --- a/packages/integration-tests-next/fixtures/vite6/package.json +++ b/packages/integration-tests-next/fixtures/vite6/package.json @@ -9,6 +9,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/vite-plugin": "file:../../../vite-plugin/sentry-vite-plugin-5.3.0.tgz", diff --git a/packages/integration-tests-next/fixtures/vite7/package.json b/packages/integration-tests-next/fixtures/vite7/package.json index 9a7ea880..14b5d21e 100644 --- a/packages/integration-tests-next/fixtures/vite7/package.json +++ b/packages/integration-tests-next/fixtures/vite7/package.json @@ -11,6 +11,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/vite-plugin": "file:../../../vite-plugin/sentry-vite-plugin-5.3.0.tgz", diff --git a/packages/integration-tests-next/fixtures/vite8/package.json b/packages/integration-tests-next/fixtures/vite8/package.json index 6843a41a..0ee48e39 100644 --- a/packages/integration-tests-next/fixtures/vite8/package.json +++ b/packages/integration-tests-next/fixtures/vite8/package.json @@ -11,6 +11,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/rollup-plugin": "file:../../../rollup-plugin/sentry-rollup-plugin-5.3.0.tgz", "@sentry/vite-plugin": "file:../../../vite-plugin/sentry-vite-plugin-5.3.0.tgz", diff --git a/packages/integration-tests-next/fixtures/webpack5/package.json b/packages/integration-tests-next/fixtures/webpack5/package.json index 7b433701..20bf49a2 100644 --- a/packages/integration-tests-next/fixtures/webpack5/package.json +++ b/packages/integration-tests-next/fixtures/webpack5/package.json @@ -12,6 +12,7 @@ }, "pnpm": { "overrides": { + "@sentry/bundler-plugins": "file:../../../bundler-plugins/sentry-bundler-plugins-5.3.0.tgz", "@sentry/bundler-plugin-core": "file:../../../bundler-plugin-core/sentry-bundler-plugin-core-5.3.0.tgz", "@sentry/webpack-plugin": "file:../../../webpack-plugin/sentry-webpack-plugin-5.3.0.tgz", "@sentry/babel-plugin-component-annotate": "file:../../../babel-plugin-component-annotate/sentry-babel-plugin-component-annotate-5.3.0.tgz" diff --git a/packages/rollup-plugin/package.json b/packages/rollup-plugin/package.json index bef25022..8bfa911a 100644 --- a/packages/rollup-plugin/package.json +++ b/packages/rollup-plugin/package.json @@ -44,12 +44,10 @@ "clean:all": "run-p clean clean:deps", "clean:build": "premove ./dist *.tgz", "clean:deps": "premove node_modules", - "test": "vitest run", "prepack": "node ./prepack.mjs" }, "dependencies": { - "@sentry/bundler-plugin-core": "5.3.0", - "magic-string": "~0.30.8" + "@sentry/bundler-plugins": "5.3.0" }, "peerDependencies": { "rollup": ">=3.2.0" @@ -62,7 +60,6 @@ "devDependencies": { "@sentry-internal/dev-utils": "5.3.0", "@types/node": "^18.6.3", - "vitest": "^4.0.0", "premove": "^4.0.0", "rolldown": "^1.0.0" }, diff --git a/packages/rollup-plugin/rollup.config.mjs b/packages/rollup-plugin/rollup.config.mjs index 98d8b5dd..4e04614e 100644 --- a/packages/rollup-plugin/rollup.config.mjs +++ b/packages/rollup-plugin/rollup.config.mjs @@ -1,9 +1,11 @@ import packageJson from "./package.json" with { type: "json" }; +const deps = Object.keys(packageJson.dependencies ?? {}); + export default { platform: "node", input: ["src/index.ts"], - external: Object.keys(packageJson.dependencies), + external: (id) => deps.some((dep) => id === dep || id.startsWith(`${dep}/`)), output: [ { file: packageJson.module, diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 21c8ce7a..bd60cf6c 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -1,265 +1 @@ -import type { Options } from "@sentry/bundler-plugin-core"; -import { - createSentryBuildPluginManager, - generateReleaseInjectorCode, - generateModuleMetadataInjectorCode, - isJsFile, - shouldSkipCodeInjection, - getDebugIdSnippet, - stringToUUID, - COMMENT_USE_STRICT_REGEX, - createDebugIdUploadFunction, - globFiles, - createComponentNameAnnotateHooks, - replaceBooleanFlagsInCode, - CodeInjection, -} from "@sentry/bundler-plugin-core"; -import type { SourceMap } from "magic-string"; -import MagicString from "magic-string"; -import type { TransformResult } from "rollup"; -import * as path from "node:path"; -import { createRequire } from "node:module"; - -function hasExistingDebugID(code: string): boolean { - // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI) - const chunkStartSnippet = code.slice(0, 6000); - const chunkEndSnippet = code.slice(-500); - - if ( - chunkStartSnippet.includes("_sentryDebugIdIdentifier") || - chunkEndSnippet.includes("//# debugId=") - ) { - return true; // Debug ID already present, skip injection - } - - return false; -} - -function getRollupMajorVersion(): string | undefined { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Rollup already transpiles this for us - const req = createRequire(import.meta.url); - const rollup = req("rollup") as { VERSION?: string }; - return rollup.VERSION?.split(".")[0]; - } catch { - // do nothing, we'll just not report a version - } - - return undefined; -} - -/** - * @ignore - this is the internal plugin factory function only used for the Vite plugin! - */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function _rollupPluginInternal( - userOptions: Options = {}, - buildTool: "rollup" | "vite", - buildToolMajorVersion?: string -) { - const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { - loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? `[sentry-${buildTool}-plugin]`, - buildTool, - buildToolMajorVersion: buildToolMajorVersion || getRollupMajorVersion(), - }); - - const { - logger, - normalizedOptions: options, - bundleSizeOptimizationReplacementValues: replacementValues, - bundleMetadata, - createDependencyOnBuildArtifacts, - } = sentryBuildPluginManager; - - if (options.disable) { - return { - name: "sentry-noop-plugin", - }; - } - - if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) { - logger.warn( - "Running Sentry plugin from within a `node_modules` folder. Some features may not work." - ); - } - - const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); - const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); - const sourcemapsEnabled = options.sourcemaps?.disable !== true; - const staticInjectionCode = new CodeInjection(); - - if (!options.release.inject) { - logger.debug( - "Release injection disabled via `release.inject` option. Will not inject release." - ); - } else if (!options.release.name) { - logger.debug( - "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." - ); - } else { - staticInjectionCode.append( - generateReleaseInjectorCode({ - release: options.release.name, - injectBuildInformation: options._experiments.injectBuildInformation || false, - }) - ); - } - - if (Object.keys(bundleMetadata).length > 0) { - staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata)); - } - - const transformAnnotations = options.reactComponentAnnotation?.enabled - ? createComponentNameAnnotateHooks( - options.reactComponentAnnotation?.ignoredComponents || [], - !!options.reactComponentAnnotation?._experimentalInjectIntoHtml - ) - : undefined; - - const transformReplace = Object.keys(replacementValues).length > 0; - const shouldTransform = transformAnnotations || transformReplace; - - function buildStart(): void { - void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { - // Telemetry failures are acceptable - }); - } - - async function transform(code: string, id: string): Promise { - // Component annotations are only in user code and boolean flag replacements are - // only in Sentry code. If we successfully add annotations, we can return early. - - if (transformAnnotations?.transform) { - const result = await transformAnnotations.transform(code, id); - if (result) { - return result; - } - } - - if (transformReplace) { - return replaceBooleanFlagsInCode(code, replacementValues); - } - - return null; - } - - function renderChunk( - code: string, - chunk: { fileName: string; facadeModuleId?: string | null }, - _?: unknown, - meta?: { magicString?: MagicString } - ): { - code: string; - map?: SourceMap; - } | null { - if (!isJsFile(chunk.fileName)) { - return null; // returning null means not modifying the chunk at all - } - - // Skip empty chunks and HTML facade chunks (Vite MPA) - if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) { - return null; - } - - const injectCode = staticInjectionCode.clone(); - - if (sourcemapsEnabled && !hasExistingDebugID(code)) { - const debugId = stringToUUID(code); // generate a deterministic debug ID - injectCode.append(getDebugIdSnippet(debugId)); - } - - if (injectCode.isEmpty()) { - return null; - } - - const ms = meta?.magicString || new MagicString(code, { filename: chunk.fileName }); - const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; - - if (match) { - // Add injected code after any comments or "use strict" at the beginning of the bundle. - ms.appendLeft(match.length, injectCode.code()); - } else { - // ms.replace() doesn't work when there is an empty string match (which happens if - // there is neither, a comment, nor a "use strict" at the top of the chunk) so we - // need this special case here. - ms.prepend(injectCode.code()); - } - - // Rolldown can pass a native MagicString instance in meta.magicString - // https://rolldown.rs/in-depth/native-magic-string#usage-examples - if (ms?.constructor?.name === "BindingMagicString") { - // Rolldown docs say to return the magic string instance directly in this case - return { code: ms as unknown as string }; - } - - return { - code: ms.toString(), - map: ms.generateMap({ file: chunk.fileName, hires: "boundary" as unknown as undefined }), - }; - } - - async function writeBundle( - outputOptions: { dir?: string; file?: string }, - bundle: { [fileName: string]: unknown } - ): Promise { - try { - await sentryBuildPluginManager.createRelease(); - - if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") { - if (outputOptions.dir) { - const outputDir = outputOptions.dir; - const JS_AND_MAP_PATTERNS = [ - "/**/*.js", - "/**/*.mjs", - "/**/*.cjs", - "/**/*.js.map", - "/**/*.mjs.map", - "/**/*.cjs.map", - ].map((q) => `${q}?(\\?*)?(#*)`); // We want to allow query and hash strings at the end of files - const buildArtifacts = await globFiles(JS_AND_MAP_PATTERNS, { root: outputDir }); - await upload(buildArtifacts); - } else if (outputOptions.file) { - await upload([outputOptions.file]); - } else { - const buildArtifacts = Object.keys(bundle).map((asset) => - path.join(path.resolve(), asset) - ); - await upload(buildArtifacts); - } - } - } finally { - freeGlobalDependencyOnBuildArtifacts(); - await sentryBuildPluginManager.deleteArtifacts(); - } - } - - const name = `sentry-${buildTool}-plugin`; - - if (shouldTransform) { - return { - name, - buildStart, - transform, - renderChunk, - writeBundle, - }; - } - - return { - name, - buildStart, - renderChunk, - writeBundle, - }; -} - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any -export function sentryRollupPlugin(userOptions: Options = {}): any { - // We return an array here so we don't break backwards compatibility with what - // unplugin used to return - return [_rollupPluginInternal(userOptions, "rollup")]; -} - -export type { Options as SentryRollupPluginOptions } from "@sentry/bundler-plugin-core"; -export { sentryCliBinaryExists } from "@sentry/bundler-plugin-core"; +export * from "@sentry/bundler-plugins/rollup"; diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index a113eaaa..16075874 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -43,17 +43,14 @@ "clean:all": "run-p clean clean:deps", "clean:build": "premove ./dist *.tgz", "clean:deps": "premove node_modules", - "test": "vitest run", "prepack": "node ./prepack.mjs" }, "dependencies": { - "@sentry/bundler-plugin-core": "5.3.0", - "@sentry/rollup-plugin": "5.3.0" + "@sentry/bundler-plugins": "5.3.0" }, "devDependencies": { "@sentry-internal/dev-utils": "5.3.0", "@types/node": "^18.6.3", - "vitest": "^4.0.0", "premove": "^4.0.0", "rolldown": "^1.0.0" }, diff --git a/packages/vite-plugin/rollup.config.mjs b/packages/vite-plugin/rollup.config.mjs index 98d8b5dd..4e04614e 100644 --- a/packages/vite-plugin/rollup.config.mjs +++ b/packages/vite-plugin/rollup.config.mjs @@ -1,9 +1,11 @@ import packageJson from "./package.json" with { type: "json" }; +const deps = Object.keys(packageJson.dependencies ?? {}); + export default { platform: "node", input: ["src/index.ts"], - external: Object.keys(packageJson.dependencies), + external: (id) => deps.some((dep) => id === dep || id.startsWith(`${dep}/`)), output: [ { file: packageJson.module, diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index c4fc5620..d8ab78aa 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,34 +1 @@ -import type { SentryRollupPluginOptions } from "@sentry/rollup-plugin"; -import { _rollupPluginInternal } from "@sentry/rollup-plugin"; -import { createRequire } from "node:module"; - -interface SentryVitePlugin { - name: string; - enforce: "pre"; -} - -function getViteMajorVersion(): string | undefined { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Rollup already transpiles this for us - const req = createRequire(import.meta.url); - const vite = req("vite") as { version?: string }; - return vite.version?.split(".")[0]; - } catch { - // do nothing, we'll just not report a version - } - - return undefined; -} - -export const sentryVitePlugin = (options?: SentryRollupPluginOptions): SentryVitePlugin[] => { - return [ - { - enforce: "pre", - ..._rollupPluginInternal(options, "vite", getViteMajorVersion()), - }, - ]; -}; - -export type { Options as SentryVitePluginOptions } from "@sentry/bundler-plugin-core"; -export { sentryCliBinaryExists } from "@sentry/bundler-plugin-core"; +export * from "@sentry/bundler-plugins/vite"; diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index a8c39130..d3677ac3 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -48,17 +48,15 @@ "clean:all": "run-p clean clean:deps", "clean:build": "premove ./dist *.tgz", "clean:deps": "premove node_modules", - "test": "vitest run", "prepack": "node ./prepack.mjs" }, "dependencies": { - "@sentry/bundler-plugin-core": "5.3.0" + "@sentry/bundler-plugins": "5.3.0" }, "devDependencies": { "@sentry-internal/dev-utils": "5.3.0", "@types/node": "^18.6.3", "@types/webpack": "npm:@types/webpack@^4", - "vitest": "^4.0.0", "premove": "^4.0.0", "rolldown": "^1.0.0", "webpack": "5.76.0" diff --git a/packages/webpack-plugin/rollup.config.mjs b/packages/webpack-plugin/rollup.config.mjs index 3895fc9c..0b39609f 100644 --- a/packages/webpack-plugin/rollup.config.mjs +++ b/packages/webpack-plugin/rollup.config.mjs @@ -3,8 +3,11 @@ import modulePackage from "module"; export default { platform: "node", - input: ["src/index.ts", "src/webpack5.ts", "src/component-annotation-transform.ts"], - external: [...Object.keys(packageJson.dependencies), ...modulePackage.builtinModules, "webpack"], + input: ["src/index.ts", "src/webpack5.ts"], + external: (id) => + [...Object.keys(packageJson.dependencies), ...modulePackage.builtinModules].some( + (dep) => id === dep || id.startsWith(`${dep}/`) + ), output: [ { dir: "./dist/esm", diff --git a/packages/webpack-plugin/src/index.ts b/packages/webpack-plugin/src/index.ts index 8e329568..8319369d 100644 --- a/packages/webpack-plugin/src/index.ts +++ b/packages/webpack-plugin/src/index.ts @@ -1,18 +1 @@ -import type { SentryWebpackPluginOptions } from "./webpack4and5"; -import { sentryWebpackPluginFactory } from "./webpack4and5"; -import * as webpack4or5 from "webpack"; - -const BannerPlugin = webpack4or5?.BannerPlugin || webpack4or5?.default?.BannerPlugin; - -const DefinePlugin = webpack4or5?.DefinePlugin || webpack4or5?.default?.DefinePlugin; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = - sentryWebpackPluginFactory({ - BannerPlugin, - DefinePlugin, - }); - -export { sentryCliBinaryExists } from "@sentry/bundler-plugin-core"; - -export type { SentryWebpackPluginOptions }; +export * from "@sentry/bundler-plugins/webpack"; diff --git a/packages/webpack-plugin/src/webpack5.ts b/packages/webpack-plugin/src/webpack5.ts index fb77007c..86e5b39d 100644 --- a/packages/webpack-plugin/src/webpack5.ts +++ b/packages/webpack-plugin/src/webpack5.ts @@ -1,12 +1 @@ -import type { SentryWebpackPluginOptions } from "./webpack4and5"; -import { sentryWebpackPluginFactory } from "./webpack4and5"; - -const createSentryWebpackPlugin = sentryWebpackPluginFactory(); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = - createSentryWebpackPlugin; - -export { sentryCliBinaryExists } from "@sentry/bundler-plugin-core"; - -export type { SentryWebpackPluginOptions }; +export * from "@sentry/bundler-plugins/webpack5"; diff --git a/yarn.lock b/yarn.lock index 9db5b31f..fab98a70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.0": +"@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== @@ -10,7 +10,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@babel/code-frame@^7.16.7", "@babel/code-frame@^7.23.5": +"@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== @@ -23,27 +23,6 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== -"@babel/core@7.18.5": - version "7.18.5" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.18.5.tgz#c597fa680e58d571c28dda9827669c78cdd7f000" - integrity sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.18.2" - "@babel/helper-compilation-targets" "^7.18.2" - "@babel/helper-module-transforms" "^7.18.0" - "@babel/helpers" "^7.18.2" - "@babel/parser" "^7.18.5" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.18.5" - "@babel/types" "^7.18.4" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.1" - semver "^6.3.0" - "@babel/core@^7.18.5": version "7.24.0" resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz#56cbda6b185ae9d9bed369816a8f4423c5f2ff1b" @@ -65,7 +44,7 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.18.2", "@babel/generator@^7.23.6": +"@babel/generator@^7.23.6": version "7.23.6" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== @@ -82,7 +61,7 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-compilation-targets@^7.18.2", "@babel/helper-compilation-targets@^7.23.6": +"@babel/helper-compilation-targets@^7.23.6": version "7.23.6" resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== @@ -120,7 +99,7 @@ dependencies: "@babel/types" "^7.22.15" -"@babel/helper-module-transforms@^7.18.0", "@babel/helper-module-transforms@^7.23.3": +"@babel/helper-module-transforms@^7.23.3": version "7.23.3" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== @@ -165,7 +144,7 @@ resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== -"@babel/helpers@^7.18.2", "@babel/helpers@^7.24.0": +"@babel/helpers@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz#a3dd462b41769c95db8091e49cfe019389a9409b" integrity sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA== @@ -183,7 +162,7 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.18.5", "@babel/parser@^7.20.7", "@babel/parser@^7.24.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac" integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg== @@ -240,7 +219,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.22.5" "@babel/plugin-transform-react-pure-annotations" "^7.23.3" -"@babel/template@^7.16.7", "@babel/template@^7.22.15", "@babel/template@^7.24.0": +"@babel/template@^7.22.15", "@babel/template@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== @@ -249,7 +228,7 @@ "@babel/parser" "^7.24.0" "@babel/types" "^7.24.0" -"@babel/traverse@^7.18.5", "@babel/traverse@^7.24.0": +"@babel/traverse@^7.24.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" integrity sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== @@ -265,7 +244,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.4", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.23.6", "@babel/types@^7.24.0", "@babel/types@^7.3.0": +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.23.6", "@babel/types@^7.24.0", "@babel/types@^7.3.0": version "7.24.0" resolved "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== @@ -289,13 +268,6 @@ dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" - integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== - dependencies: - tslib "^2.4.0" - "@emnapi/wasi-threads@1.2.1": version "1.2.1" resolved "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" @@ -2055,11 +2027,6 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -3160,7 +3127,7 @@ json-schema-traverse@^0.4.1: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: +json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -3348,7 +3315,7 @@ ms@2.1.3: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.3.11, nanoid@^3.3.6: +nanoid@^3.3.11: version "3.3.11" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== @@ -3966,7 +3933,7 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.3.0, semver@^6.3.1: +semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -4053,7 +4020,7 @@ signal-exit@^3.0.2: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -source-map-js@^1.0.2, source-map-js@^1.2.1: +source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==