@@ -25,6 +25,7 @@ import {
2525import { playGeigerClickSound } from '@web-utils/geiger' ;
2626import { ICONS } from '@web-assets/svgs/svgs' ;
2727import { updateFiberRenderData , type RenderData } from 'src/core/utils' ;
28+ import { readLocalStorage , saveLocalStorage } from '@web-utils/helpers' ;
2829import { initReactScanOverlay } from './web/overlay' ;
2930import { createInstrumentation , type Render } from './instrumentation' ;
3031import { createToolbar } from './web/toolbar' ;
@@ -33,6 +34,7 @@ import { type getSession } from './monitor/utils';
3334import styles from './web/assets/css/styles.css' ;
3435
3536let toolbarContainer : HTMLElement | null = null ;
37+ let shadowRoot : ShadowRoot | null = null ;
3638
3739export interface Options {
3840 /**
@@ -213,6 +215,74 @@ export const ReactScanInternals: Internals = {
213215 Store,
214216} ;
215217
218+ type LocalStorageOptions = Omit < Options ,
219+ | 'onCommitStart'
220+ | 'onRender'
221+ | 'onCommitFinish'
222+ | 'onPaintStart'
223+ | 'onPaintFinish'
224+ > ;
225+
226+ const validateOptions = ( options : Partial < Options > ) : Partial < Options > => {
227+ const errors : Array < string > = [ ] ;
228+ const validOptions : Partial < Options > = { } ;
229+
230+ Object . entries ( options ) . forEach ( ( [ key , value ] ) => {
231+ switch ( key ) {
232+ case 'enabled' :
233+ case 'includeChildren' :
234+ case 'playSound' :
235+ case 'log' :
236+ case 'showToolbar' :
237+ case 'report' :
238+ case 'alwaysShowLabels' :
239+ case 'dangerouslyForceRunInProduction' :
240+ if ( typeof value !== 'boolean' ) {
241+ errors . push ( `- ${ key } must be a boolean. Got "${ value } "` ) ;
242+ } else {
243+ ( validOptions as any ) [ key ] = value ;
244+ }
245+ break ;
246+ case 'renderCountThreshold' :
247+ case 'resetCountTimeout' :
248+ if ( typeof value !== 'number' || value < 0 ) {
249+ errors . push ( `- ${ key } must be a non-negative number. Got "${ value } "` ) ;
250+ } else {
251+ ( validOptions as any ) [ key ] = value ;
252+ }
253+ break ;
254+ case 'animationSpeed' :
255+ if ( ! [ 'slow' , 'fast' , 'off' ] . includes ( value as string ) ) {
256+ errors . push ( `- Invalid animation speed "${ value } ". Using default "fast"` ) ;
257+ } else {
258+ ( validOptions as any ) [ key ] = value ;
259+ }
260+ break ;
261+ case 'onCommitStart' :
262+ case 'onCommitFinish' :
263+ case 'onRender' :
264+ case 'onPaintStart' :
265+ case 'onPaintFinish' :
266+ if ( typeof value !== 'function' ) {
267+ errors . push ( `- ${ key } must be a function. Got "${ value } "` ) ;
268+ } else {
269+ ( validOptions as any ) [ key ] = value ;
270+ }
271+ break ;
272+ default :
273+ errors . push ( `- Unknown option "${ key } "` ) ;
274+ }
275+ } ) ;
276+
277+ if ( errors . length > 0 ) {
278+ // eslint-disable-next-line no-console
279+ console . warn ( `[React Scan] Invalid options:\n${ errors . join ( '\n' ) } ` ) ;
280+ return { } ;
281+ }
282+
283+ return validOptions ;
284+ } ;
285+
216286export const getReport = ( type ?: React . ComponentType < any > ) => {
217287 if ( type ) {
218288 for ( const reportData of Array . from ( Store . legacyReportData . values ( ) ) ) {
@@ -225,32 +295,57 @@ export const getReport = (type?: React.ComponentType<any>) => {
225295 return Store . legacyReportData ;
226296} ;
227297
228- export const setOptions = ( options : Options ) => {
298+ const initializeScanOptions = ( userOptions ?: Partial < Options > ) => {
299+ const options = ReactScanInternals . options . value ;
300+
301+ const localStorageOptions = readLocalStorage < LocalStorageOptions > ( 'react-scan-options' ) ;
302+ if ( localStorageOptions ) {
303+ ( Object . keys ( localStorageOptions ) as Array < keyof LocalStorageOptions > ) . forEach ( key => {
304+ const value = localStorageOptions [ key ] ;
305+ if ( key in options && value !== null ) {
306+ ( options as any ) [ key ] = value ;
307+ }
308+ } ) ;
309+ }
310+
311+ ReactScanInternals . options . value = validateOptions ( {
312+ ...options ,
313+ ...userOptions ,
314+ } ) ;
315+
316+ saveLocalStorage ( 'react-scan-options' , ReactScanInternals . options . value ) ;
317+
318+ return ReactScanInternals . options . value ;
319+ } ;
320+
321+ export const setOptions = ( userOptions : Partial < Options > ) => {
322+ const validOptions = validateOptions ( userOptions ) ;
323+
324+ if ( Object . keys ( validOptions ) . length === 0 ) {
325+ return ;
326+ }
327+
229328 const { instrumentation } = ReactScanInternals ;
230329 if ( instrumentation ) {
231- instrumentation . isPaused . value = options . enabled === false ;
330+ instrumentation . isPaused . value = validOptions . enabled === false ;
232331 }
233332
234- const previousOptions = ReactScanInternals . options . value ;
333+ const newOptions = initializeScanOptions ( validOptions ) ;
235334
236- ReactScanInternals . options . value = {
237- ...ReactScanInternals . options . value ,
238- ...options ,
239- } ;
335+ if ( toolbarContainer && ! newOptions . showToolbar ) {
336+ toolbarContainer . remove ( ) ;
337+ }
240338
241- if ( previousOptions . showToolbar && ! options . showToolbar ) {
242- if ( toolbarContainer ) {
243- toolbarContainer . remove ( ) ;
244- toolbarContainer = null ;
245- }
339+ if ( newOptions . showToolbar && toolbarContainer && shadowRoot ) {
340+ toolbarContainer = createToolbar ( shadowRoot ) ;
246341 }
247342} ;
248343
249344export const getOptions = ( ) => ReactScanInternals . options ;
250345
251346export const reportRender = ( fiber : Fiber , renders : Array < Render > ) => {
252347 const reportFiber = fiber ;
253- const { selfTime } = getTimings ( fiber ) ;
348+ const { selfTime } = getTimings ( fiber . type ) ;
254349 const displayName = getDisplayName ( fiber . type ) ;
255350
256351 Store . lastReportTime . value = performance . now ( ) ;
@@ -354,6 +449,11 @@ export const start = () => {
354449 ! ReactScanInternals . options . value . dangerouslyForceRunInProduction
355450 ) {
356451 setOptions ( { enabled : false , showToolbar : false } ) ;
452+ // eslint-disable-next-line no-console
453+ console . warn (
454+ '[React Scan] Running in production mode is not recommended.\n' +
455+ 'If you really need this, set dangerouslyForceRunInProduction: true in options.'
456+ ) ;
357457 return ;
358458 }
359459
@@ -365,7 +465,7 @@ export const start = () => {
365465 const container = document . createElement ( 'div' ) ;
366466 container . id = 'react-scan-root' ;
367467
368- const shadow = container . attachShadow ( { mode : 'open' } ) ;
468+ shadowRoot = container . attachShadow ( { mode : 'open' } ) ;
369469
370470 const fragment = document . createDocumentFragment ( ) ;
371471
@@ -376,7 +476,7 @@ export const start = () => {
376476 ICONS ,
377477 'image/svg+xml' ,
378478 ) . documentElement ;
379- shadow . appendChild ( iconSprite ) ;
479+ shadowRoot . appendChild ( iconSprite ) ;
380480
381481 const root = document . createElement ( 'div' ) ;
382482 root . id = 'react-scan-toolbar-root' ;
@@ -385,22 +485,22 @@ export const start = () => {
385485 fragment . appendChild ( cssStyles ) ;
386486 fragment . appendChild ( root ) ;
387487
388- shadow . appendChild ( fragment ) ;
488+ shadowRoot . appendChild ( fragment ) ;
389489
390490 document . documentElement . appendChild ( container ) ;
391491
392492 ctx = initReactScanOverlay ( ) ;
393493 if ( ! ctx ) return ;
394494 startFlushOutlineInterval ( ctx ) ;
395495
396- createInspectElementStateMachine ( shadow ) ;
496+ createInspectElementStateMachine ( shadowRoot ) ;
397497
398498 globalThis . __REACT_SCAN__ = {
399499 ReactScanInternals,
400500 } ;
401501
402502 if ( ReactScanInternals . options . value . showToolbar ) {
403- toolbarContainer = createToolbar ( shadow ) ;
503+ toolbarContainer = createToolbar ( shadowRoot ) ;
404504 }
405505
406506 container . setAttribute ( 'part' , 'scan-root' ) ;
0 commit comments