@@ -3,11 +3,13 @@ import type * as React from 'react';
33import { type Signal , signal } from '@preact/signals' ;
44import {
55 getDisplayName ,
6+ getRDTHook ,
67 getTimings ,
78 getType ,
89 isCompositeFiber ,
910 isInstrumentationActive ,
1011 traverseFiber ,
12+ detectReactBuildType ,
1113} from 'bippy' ;
1214import {
1315 type ActiveOutline ,
@@ -49,6 +51,13 @@ export interface Options {
4951 */
5052 includeChildren ?: boolean ;
5153
54+ /**
55+ * Force React Scan to run in production (not recommended)
56+ *
57+ * @default false
58+ */
59+ dangerouslyForceRunInProduction ?: boolean ;
60+
5261 /**
5362 * Enable/disable geiger sound
5463 *
@@ -196,6 +205,7 @@ export const ReactScanInternals: Internals = {
196205 report : undefined ,
197206 alwaysShowLabels : false ,
198207 animationSpeed : 'fast' ,
208+ dangerouslyForceRunInProduction : false ,
199209 } ) ,
200210 onRender : null ,
201211 scheduledOutlines : [ ] ,
@@ -307,65 +317,96 @@ export const isValidFiber = (fiber: Fiber) => {
307317export const start = ( ) => {
308318 if ( typeof window === 'undefined' ) return ;
309319
310- const existingRoot = document . querySelector ( 'react-scan-root' ) ;
311- if ( existingRoot ) {
312- return ;
313- }
320+ const audioContext =
321+ typeof window !== 'undefined'
322+ ? new ( window . AudioContext ||
323+ // @ts -expect-error -- This is a fallback for Safari
324+ window . webkitAudioContext ) ( )
325+ : null ;
314326
315- // Create container for shadow DOM
316- const container = document . createElement ( 'div' ) ;
317- container . id = 'react-scan-root' ;
327+ let ctx : ReturnType < typeof initReactScanOverlay > | null = null ;
328+ // TODO: dynamic enable, and inspect-off check
318329
319- const shadow = container . attachShadow ( { mode : 'open' } ) ;
330+ const instrumentation = createInstrumentation ( 'devtools' , {
331+ onActive ( ) {
332+ if ( ! Store . monitor . value ) {
333+ const rdtHook = getRDTHook ( ) ;
334+ let isProduction = false ;
335+ for ( const renderer of rdtHook . renderers . values ( ) ) {
336+ const buildType = detectReactBuildType ( renderer ) ;
337+ if (
338+ buildType === 'production' &&
339+ ! ReactScanInternals . options . value . dangerouslyForceRunInProduction
340+ ) {
341+ isProduction = true ;
342+ }
343+ }
344+ if ( isProduction ) {
345+ setOptions ( { enabled : false , showToolbar : false } ) ;
346+ return ;
347+ }
320348
321- const fragment = document . createDocumentFragment ( ) ;
349+ const existingRoot = document . querySelector ( 'react-scan-root' ) ;
350+ if ( existingRoot ) {
351+ return ;
352+ }
322353
323- // add styles
324- const cssStyles = document . createElement ( 'style' ) ;
325- cssStyles . textContent = styles ;
354+ const container = document . createElement ( 'div' ) ;
355+ container . id = 'react-scan-root' ;
326356
327- // Create SVG sprite sheet node directly
328- const iconSprite = new DOMParser ( ) . parseFromString (
329- ICONS ,
330- 'image/svg+xml' ,
331- ) . documentElement ;
332- shadow . appendChild ( iconSprite ) ;
357+ const shadow = container . attachShadow ( { mode : 'open' } ) ;
333358
334- // add toolbar root
335- const root = document . createElement ( 'div' ) ;
336- root . id = 'react-scan-toolbar-root' ;
337- root . className = 'absolute z-2147483647' ;
359+ const fragment = document . createDocumentFragment ( ) ;
338360
339- fragment . appendChild ( cssStyles ) ;
340- fragment . appendChild ( root ) ;
361+ const cssStyles = document . createElement ( 'style' ) ;
362+ cssStyles . textContent = styles ;
341363
342- shadow . appendChild ( fragment ) ;
364+ const iconSprite = new DOMParser ( ) . parseFromString (
365+ ICONS ,
366+ 'image/svg+xml' ,
367+ ) . documentElement ;
368+ shadow . appendChild ( iconSprite ) ;
343369
344- // Add container to document first (so shadow DOM is available)
345- document . documentElement . appendChild ( container ) ;
370+ const root = document . createElement ( 'div' ) ;
371+ root . id = 'react-scan-toolbar-root' ;
372+ root . className = 'absolute z-2147483647' ;
346373
347- const options = ReactScanInternals . options . value ;
374+ fragment . appendChild ( cssStyles ) ;
375+ fragment . appendChild ( root ) ;
348376
349- const ctx = initReactScanOverlay ( ) ;
350- if ( ! ctx ) return ;
377+ shadow . appendChild ( fragment ) ;
351378
352- createInspectElementStateMachine ( shadow ) ;
379+ document . documentElement . appendChild ( container ) ;
353380
354- const audioContext =
355- typeof window !== 'undefined'
356- ? new ( window . AudioContext ||
357- // @ts -expect-error -- This is a fallback for Safari
358- window . webkitAudioContext ) ( )
359- : null ;
381+ ctx = initReactScanOverlay ( ) ;
382+ if ( ! ctx ) return ;
360383
361- globalThis . __REACT_SCAN__ = {
362- ReactScanInternals,
363- } ;
384+ createInspectElementStateMachine ( shadow ) ;
385+
386+ globalThis . __REACT_SCAN__ = {
387+ ReactScanInternals,
388+ } ;
389+
390+ if ( ReactScanInternals . options . value . showToolbar ) {
391+ toolbarContainer = createToolbar ( shadow ) ;
392+ }
393+
394+ // Add this right after creating the container
395+ container . setAttribute ( 'part' , 'scan-root' ) ;
396+
397+ // Add this before creating the Shadow DOM
398+ const mainStyles = document . createElement ( 'style' ) ;
399+ mainStyles . textContent = `
400+ html[data-theme="light"] react-scan-root::part(scan-root) {
401+ --icon-color: rgba(0, 0, 0, 0.8);
402+ }
403+
404+ html[data-theme="dark"] react-scan-root::part(scan-root) {
405+ --icon-color: rgba(255, 255, 255, 0.8);
406+ }
407+ ` ;
408+ document . head . appendChild ( mainStyles ) ;
364409
365- // TODO: dynamic enable, and inspect-off check
366- const instrumentation = createInstrumentation ( 'devtools' , {
367- onActive ( ) {
368- if ( ! Store . monitor . value ) {
369410 const existingOverlay = document . querySelector ( 'react-scan-overlay' ) ;
370411 if ( existingOverlay ) {
371412 return ;
@@ -388,7 +429,7 @@ export const start = () => {
388429 } ,
389430 isValidFiber,
390431 onRender ( fiber , renders ) {
391- if ( ReactScanInternals . instrumentation ?. isPaused . value ) {
432+ if ( Boolean ( ReactScanInternals . instrumentation ?. isPaused . value ) || ! ctx ) {
392433 // don't draw if it's paused
393434 return ;
394435 }
@@ -430,26 +471,6 @@ export const start = () => {
430471
431472 ReactScanInternals . instrumentation = instrumentation ;
432473
433- if ( options . showToolbar ) {
434- toolbarContainer = createToolbar ( shadow ) ;
435- }
436-
437- // Add this right after creating the container
438- container . setAttribute ( 'part' , 'scan-root' ) ;
439-
440- // Add this before creating the Shadow DOM
441- const mainStyles = document . createElement ( 'style' ) ;
442- mainStyles . textContent = `
443- html[data-theme="light"] react-scan-root::part(scan-root) {
444- --icon-color: rgba(0, 0, 0, 0.8);
445- }
446-
447- html[data-theme="dark"] react-scan-root::part(scan-root) {
448- --icon-color: rgba(255, 255, 255, 0.8);
449- }
450- ` ;
451- document . head . appendChild ( mainStyles ) ;
452-
453474 // TODO: add an visual error indicator that it didn't load
454475 if ( ! Store . monitor . value ) {
455476 setTimeout ( ( ) => {
0 commit comments