Skip to content

Commit

Permalink
feat: introduce synchronous env detection
Browse files Browse the repository at this point in the history
  • Loading branch information
molefrog committed Oct 4, 2022
1 parent 709e256 commit 6ba1fd0
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 118 deletions.
14 changes: 6 additions & 8 deletions __tests__/detect-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { Env } from '../src/env.types'

describe('Detect user env', () => {
describe('Preact', () => {
it('should detect preact if synthetic event was not detected', () => {
it('should detect preact if class components receive any arguments in render', () => {
const env = detectEnvironment({
context: {
syntheticEventDetected: false,
classRenderReceivesAnyArguments: true,
},
})

Expand All @@ -17,10 +17,10 @@ describe('Detect user env', () => {
})

describe('React', () => {
it('should detect react if synthetic event was detect', () => {
it('should detect react if class component receives no args in render', () => {
const env = detectEnvironment({
context: {
syntheticEventDetected: true,
classRenderReceivesAnyArguments: false,
},
})

Expand All @@ -39,7 +39,7 @@ describe('Detect user env', () => {

const env = detectEnvironment({
context: {
syntheticEventDetected: true,
classRenderReceivesAnyArguments: false,
},
})

Expand All @@ -58,9 +58,7 @@ describe('Detect user env', () => {
})

const env = detectEnvironment({
context: {
syntheticEventDetected: true,
},
context: { classRenderReceivesAnyArguments: false },
})

expect(env).toEqual({
Expand Down
39 changes: 0 additions & 39 deletions __tests__/synthetic-event-detector.test.tsx

This file was deleted.

24 changes: 24 additions & 0 deletions __tests__/with-environment.preact.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { render as preactRender } from '@testing-library/preact'
import { h } from 'preact'

describe('WithEnvironment', () => {
describe('when running within Preact', () => {
beforeEach(() => {
jest.doMock('react-dom', () => require('preact/compat'))
jest.doMock('react', () => require('preact/compat'))
})

afterEach(() => {
jest.resetModules()
})

it('should detect env as "preact"', () => {
const { WithEnvironment } = require('../src/components/with-environment')
const PrintEnv = (props: any) => h('div', null, props?.env?.name)

const { container } = preactRender(h(WithEnvironment, null, h(PrintEnv, null)))

expect(container.innerHTML).toContain('preact')
})
})
})
30 changes: 30 additions & 0 deletions __tests__/with-environment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { FunctionComponent } from 'react'
import { render } from '@testing-library/react'

import { WithEnvironment } from '../src/components/with-environment'

describe('WithEnvironment', () => {
it('enhances provided element with `env` prop', () => {
const Mock = jest.fn(() => <div>foo</div>) as FunctionComponent

render(
<WithEnvironment>
<Mock />
</WithEnvironment>
)

expect(Mock).toHaveBeenCalledWith(expect.objectContaining({ env: expect.any(Object) }), expect.anything())
})

it('keeps the original props of the element', () => {
const Echo = ({ message }: { message: string }) => <span>{message}</span>

const { container } = render(
<WithEnvironment>
<Echo message='hello' />
</WithEnvironment>
)

expect(container.innerHTML).toContain('hello')
})
})
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
"@semantic-release/github": "8.0.4",
"@semantic-release/npm": "9.0.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/preact": "^3.2.2",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^27.4.0",
Expand All @@ -78,6 +80,7 @@
"husky": "^8.0.1",
"jest": "^27.5.1",
"lint-staged": "^13.0.3",
"preact": "^10.11.0",
"prettier": "^2.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
70 changes: 34 additions & 36 deletions src/components/fpjs-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { PropsWithChildren, useCallback, useEffect, useMemo, useRef } from 'react'
import FpjsContext from '../fpjs-context'
import { FpjsClient, FpjsClientOptions } from '@fingerprintjs/fingerprintjs-pro-spa'
import * as packageInfo from '../../package.json'
import { isSSR } from '../ssr'
import { getEnvironment } from '../get-env'
import { type DetectEnvContext } from '../detect-env'
import type { EnvDetails } from '../env.types'
import { SyntheticEventDetector } from './synthetic-event-detector'
import { waitUntil } from '../utils/wait-until'
import { WithEnvironment } from './with-environment'
import type { EnvDetails } from '../env.types'

const pkgName = packageInfo.name.split('/')[1]

Expand All @@ -34,21 +32,40 @@ interface FpjsProviderOptions extends FpjsClientOptions {
* ```
*
* Provides the FpjsContext to its child components.
*
* @privateRemarks
* This is just a wrapper around the actual provider.
* For the implementation, see `ProviderWithEnv` component.
*/
export function FpjsProvider<TExtended extends boolean>({
export function FpjsProvider<T extends boolean>(props: PropsWithChildren<FpjsProviderOptions>) {
const propsWithEnv = props as PropsWithChildren<ProviderWithEnvProps>

return (
<WithEnvironment>
<ProviderWithEnv<T> {...propsWithEnv} />
</WithEnvironment>
)
}

interface ProviderWithEnvProps extends FpjsProviderOptions {
/**
* Contains details about the env we're currently running in (e.g. framework, version)
*/
env: EnvDetails
}

function ProviderWithEnv<TExtended extends boolean>({
children,
forceRebuild,
cache,
cacheTimeInSeconds,
cachePrefix,
cacheLocation,
loadOptions,
}: PropsWithChildren<FpjsProviderOptions>) {
const [env, setEnv] = useState<EnvDetails | undefined>()
const [envContext, setEnvContext] = useState<DetectEnvContext | undefined>()

const clientInitPromiseRef = useRef<Promise<unknown>>()
env,
}: PropsWithChildren<ProviderWithEnvProps>) {
const clientRef = useRef<FpjsClient>()
const clientInitPromiseRef = useRef<Promise<unknown>>()

const clientOptions = useMemo(() => {
return {
Expand Down Expand Up @@ -81,7 +98,7 @@ export function FpjsProvider<TExtended extends boolean>({

clientInitPromiseRef.current = createdClient.init()

clientRef.current = createdClient
return createdClient
}, [clientOptions, env, loadOptions])

const getClient = useCallback(async () => {
Expand Down Expand Up @@ -115,16 +132,6 @@ export function FpjsProvider<TExtended extends boolean>({
[getClient]
)

const handleSyntheticEventResult = useCallback((isSyntheticEvent: boolean) => {
const context: DetectEnvContext = {
syntheticEventDetected: isSyntheticEvent,
}

setEnvContext(context)

setEnv(getEnvironment({ context }))
}, [])

const clearCache = useCallback(async () => {
const client = await getClient()

Expand All @@ -139,21 +146,12 @@ export function FpjsProvider<TExtended extends boolean>({
}, [clearCache, getVisitorData])

useEffect(() => {
if (env) {
createClient()
}
}, [env, createClient])

useEffect(() => {
if (forceRebuild) {
createClient()
// By default, the client is always initialized once during the first render and won't be updated
// if the configuration changes. Use `forceRebuilt` flag to disable this behaviour.
if (!clientRef.current || forceRebuild) {
clientRef.current = createClient()
}
}, [forceRebuild, clientOptions, createClient])

return (
<FpjsContext.Provider value={contextValue}>
{!envContext && <SyntheticEventDetector onResult={handleSyntheticEventResult} />}
{children}
</FpjsContext.Provider>
)
return <FpjsContext.Provider value={contextValue}>{children}</FpjsContext.Provider>
}
30 changes: 0 additions & 30 deletions src/components/synthetic-event-detector.tsx

This file was deleted.

53 changes: 53 additions & 0 deletions src/components/with-environment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Component, cloneElement } from 'react'

import { getEnvironment } from '../get-env'
import { type DetectEnvParams } from '../detect-env'
import { type EnvDetails } from '../env.types'

export interface WithEnvironmentProps {
// exactly one element must be provided
children: JSX.Element
}

/**
* Utility component that synchronously detects the current environment (React/Preact etc.) and
* provides it as a prop to the child element.
*
* @example
* ```jsx
* const App = ({ env }: { env: EnvDetails }) => `I'm running in ${env.name}!`
*
* <WithEnvironment>
* <App />
* </WithEnvironment>
* ```
*/
class WithEnvironment extends Component<WithEnvironmentProps> {
constructor(props: WithEnvironmentProps) {
super(props)
}

detectedEnv: EnvDetails | undefined

render(...args: any[]) {
if (!this.detectedEnv) {
// unlike React, class components in Preact always receive `props` and `state` in render()
// this is true for both Preact 8.x and 10.x
const hasAnyArguments = args.length > 0
const detectParams: DetectEnvParams = {
context: { classRenderReceivesAnyArguments: hasAnyArguments },
}

this.detectedEnv = getEnvironment(detectParams)
}

// passes the `env` down as a prop
return cloneElement(this.props.children, { env: this.detectedEnv })
}

shouldComponentUpdate() {
return false
}
}

export { WithEnvironment }
8 changes: 3 additions & 5 deletions src/detect-env.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Env, type EnvDetails } from './env.types'

export interface DetectEnvContext {
syntheticEventDetected: boolean
classRenderReceivesAnyArguments: boolean
}

export interface DetectEnvParams {
Expand Down Expand Up @@ -29,18 +29,16 @@ function runEnvChecks(...strategies: EnvCheckStrategy[]) {
/**
* Runs checks that determine if user is using preact.
* So far they are not ideal, as there is no consistent way to detect preact.
* Right now the main determinant is if synthetic event was not detected.
* */
function isPreact(context: DetectEnvContext) {
return !context.syntheticEventDetected
return context.classRenderReceivesAnyArguments
}

/**
* Checks if user is using react.
* So far we are doing that by checking if synthetic event was detected.
* */
function isReact(context: DetectEnvContext) {
return context.syntheticEventDetected
return !context.classRenderReceivesAnyArguments
}

/**
Expand Down
Loading

0 comments on commit 6ba1fd0

Please sign in to comment.