UNPKG

@sentry/remix

Version:
167 lines (141 loc) 5.33 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const path = require('path'); const createRemixRouteManifest = require('./createRemixRouteManifest.js'); /** * Escapes a JSON string for safe embedding in HTML script tags. * JSON.stringify alone doesn't escape </script> or <!-- which can break out of script context. */ function escapeJsonForHtml(jsonString) { return jsonString .replace(/<\//g, '<\\/') // Escape </ to prevent </script> injection .replace(/<!--/g, '<\\!--'); // Escape <!-- to prevent HTML comment injection } // Global variable key used to store the route manifest const MANIFEST_GLOBAL_KEY = '_sentryRemixRouteManifest' ; /** * Vite plugin to inject Remix route manifest for Sentry client-side route parameterization. * * @param options - Plugin configuration options * @returns Vite plugin * * @example * ```typescript * // vite.config.ts * import { defineConfig } from 'vite'; * import { vitePlugin as remix } from '@remix-run/dev'; * import { sentryRemixVitePlugin } from '@sentry/remix'; * * export default defineConfig({ * plugins: [ * remix(), * sentryRemixVitePlugin({ * appDirPath: './app', * }), * ], * }); * ``` */ function sentryRemixVitePlugin(options = {}) { let routeManifestJson = ''; let isDevMode = false; return { name: 'sentry-remix-route-manifest', enforce: 'post', configResolved(config) { isDevMode = config.command === 'serve' || config.mode === 'development'; try { const rootDir = config.root || process.cwd(); let resolvedAppDirPath = options.appDirPath; if (resolvedAppDirPath && !path.isAbsolute(resolvedAppDirPath)) { resolvedAppDirPath = path.resolve(rootDir, resolvedAppDirPath); } const manifest = createRemixRouteManifest.createRemixRouteManifest({ appDirPath: resolvedAppDirPath, rootDir, }); routeManifestJson = JSON.stringify(manifest); if (isDevMode) { // eslint-disable-next-line no-console console.log( `[Sentry Remix] Found ${manifest.staticRoutes.length} static and ${manifest.dynamicRoutes.length} dynamic routes`, ); } } catch (error) { // eslint-disable-next-line no-console console.error('[Sentry Remix] Failed to generate route manifest:', error); routeManifestJson = JSON.stringify({ dynamicRoutes: [], staticRoutes: [] }); } }, transformIndexHtml: { order: 'pre', handler(html) { if (!routeManifestJson) { return html; } /** * XSS Prevention: JSON.stringify escapes quotes/backslashes, but we also need to escape * HTML-dangerous sequences like </script> and <!-- that could break out of the script context. */ const safeJsonValue = escapeJsonForHtml(JSON.stringify(routeManifestJson)); const script = `<script>window.${MANIFEST_GLOBAL_KEY} = ${safeJsonValue};</script>`; if (/<head>/i.test(html)) { return html.replace(/<head>/i, match => `${match}\n ${script}`); } if (/<html[^>]*>/i.test(html)) { return html.replace(/<html[^>]*>/i, match => `${match}\n<head>${script}</head>`); } return `<!DOCTYPE html><html><head>${script}</head><body>${html}</body></html>`; }, }, transform(code, id) { if (!routeManifestJson) { return null; } const isClientEntry = /entry[.-]client\.[jt]sx?$/.test(id) || // Also handle Remix's default entry.client location id.includes('/entry.client.') || id.includes('/entry-client.'); const isServerEntry = /entry[.-]server\.[jt]sx?$/.test(id) || // Also handle Remix's default entry.server location id.includes('/entry.server.') || id.includes('/entry-server.') || // Also handle Hydrogen/Cloudflare Workers server files /(^|\/)server\.[jt]sx?$/.test(id); if (isClientEntry) { // XSS Prevention: Escape HTML-dangerous sequences in addition to JSON escaping const safeJsonValue = escapeJsonForHtml(JSON.stringify(routeManifestJson)); const injectedCode = ` // Sentry Remix Route Manifest - Auto-injected if (typeof window !== 'undefined') { window.${MANIFEST_GLOBAL_KEY} = window.${MANIFEST_GLOBAL_KEY} || ${safeJsonValue}; } ${code}`; return { code: injectedCode, map: null, }; } if (isServerEntry) { // Inject into server entry for server-side transaction naming // Use globalThis for Cloudflare Workers/Hydrogen compatibility // XSS Prevention: Escape HTML-dangerous sequences (important if server renders this) const safeJsonValue = escapeJsonForHtml(JSON.stringify(routeManifestJson)); const injectedCode = ` // Sentry Remix Route Manifest - Auto-injected if (typeof globalThis !== 'undefined') { globalThis.${MANIFEST_GLOBAL_KEY} = globalThis.${MANIFEST_GLOBAL_KEY} || ${safeJsonValue}; } ${code}`; return { code: injectedCode, map: null, }; } return null; }, }; } exports.sentryRemixVitePlugin = sentryRemixVitePlugin; //# sourceMappingURL=vite.js.map