Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/react-native/app.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expo-localization is an optional dep so i think we should not have this here

plugins: ['expo-localization'],
}
2 changes: 1 addition & 1 deletion packages/react-native/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = {
['@babel/plugin-transform-class-properties', { loose: true }],
],
presets: [
'module:metro-react-native-babel-preset',
'babel-preset-expo',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does that make expo mandatory? we should support bare bones react native as well

'@babel/env',
'@babel/preset-typescript',
['@babel/preset-react', { runtime: 'automatic' }],
Expand Down
25 changes: 12 additions & 13 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,31 @@
"devDependencies": {
"@babel/cli": "^7.19.3",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@expo/metro-config": "~0.7.0",
"@posthog-tooling/rollup-utils": "workspace:*",
"@posthog-tooling/tsconfig-base": "workspace:*",
"@react-native-async-storage/async-storage": "^1.17.10",
"@react-native/babel-preset": "^0.80.1",
"@react-navigation/native": "^5.0.10",
"@types/react": "^17.0.87",
"@types/react-native": "^0.69.1",
"@types/jest": "catalog:",
"expo": "^45.0.6",
"expo-application": "^4.0.0",
"expo-device": "^4.0.0",
"expo-file-system": "^13.0.0",
"expo-localization": "^11.0.0",
"@types/react": "^18.2.79",
"babel-preset-expo": "~9.3.2",
"expo": "^48.0.21",
"expo-application": "^5.1.1",
"expo-device": "^5.2.1",
"expo-file-system": "^15.2.2",
"expo-localization": "^14.1.1",
"jest": "catalog:",
"jest-environment-node": "catalog:",
"jest-expo": "catalog:",
"metro": "0.83.1",
"@expo/metro-config": "~0.20.0",
"metro": "0.73.10",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh interesting! It seems like the easiest approach might be to kill this PR and go all the way to 50 (or 53, latest) then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i am not sure what would be the correct min. version of metro here
expo 50 uses rn 0.73, so thats our min. support, we would use the metro version that is compatible with them

"posthog-react-native-session-replay": "^1.2.0",
"react": "18.2.0",
"react-native": "^0.69.1",
"react-native": "^0.71.14",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should support >= 0.73 beause of this
https://expo.dev/changelog/2024-01-18-sdk-50

expo 50 uses rn 0.73

"react-native-device-info": "^10.3.0",
"react-native-localize": "^3.0.0",
"react-native-navigation": "^6.0.0",
"react-native-safe-area-context": "^4.10.1",
"react-native-svg": "^15.0.0",
"react-native-safe-area-context": "^4.5.0",
"react-native-svg": "^13.4.0",
"ts-jest": "catalog:",
"typescript": "catalog:"
},
Expand Down
114 changes: 80 additions & 34 deletions packages/react-native/src/tooling/vendor/metro/metro.d.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,77 @@
// Vendored / modified from @facebook/metro
// Type declarations for metro 0.73.x (used by Expo 48 / React Native 0.71.x)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// https://github.com/facebook/metro/commit/9b85f83c9cc837d8cd897aa7723be7da5b296067

// MIT License
declare module 'metro' {
export interface MixedOutput {
type: string
data: {
code: string
lineCount?: number
map?: unknown[]
functionMap?: unknown
}
}

// Copyright (c) Meta Platforms, Inc. and affiliates.
export interface Module<T = MixedOutput> {
dependencies: Map<string, { absolutePath: string }>
getSource: () => Buffer
inverseDependencies: Set<string>
output: readonly T[]
path: string
}

// 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:
export interface ReadOnlyGraph<T = MixedOutput> {
dependencies: ReadonlyMap<string, Module<T>>
entryPoints: readonly string[]
importBundleNames: ReadonlySet<string>
transformOptions: {
hot: boolean
dev: boolean
minify: boolean
platform?: string | null
type: string
[key: string]: unknown
}
}

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
export interface SerializerOptions {
asyncRequireModulePath: string
createModuleId: (path: string) => number
dev: boolean
getRunModuleStatement: (moduleId: string | number) => string
includeAsyncPaths: boolean
inlineSourceMap?: boolean
modulesOnly: boolean
processModuleFilter: (module: Module<MixedOutput>) => boolean
projectRoot: string
runBeforeMainModule: readonly string[]
runModule: boolean
serverRoot: string
shouldAddToIgnoreList: (module: Module<MixedOutput>) => boolean
sourceMapUrl?: string
sourceUrl?: string
}

// 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.
export interface BundleMetadata {
pre: number
post: number
modules: [number, number][]
}

// All exports were moved from src to private in Metro 0.83.0.
// https://github.com/facebook/metro/commit/ae6f42372ed361611b5672705f22081c2022cf28
export interface MetroConfig {
serializer?: {
customSerializer?: (
entryPoint: string,
preModules: readonly Module<MixedOutput>[],
graph: ReadOnlyGraph<MixedOutput>,
options: SerializerOptions
) => Promise<string | { code: string; map: string }>
}
[key: string]: unknown
}
}

declare module 'metro/private/DeltaBundler/Serializers/baseJSBundle' {
// https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/Serializers/baseJSBundle.js#L25
declare module 'metro/src/DeltaBundler/Serializers/baseJSBundle' {
import type { Module, ReadOnlyGraph, SerializerOptions } from 'metro'
const baseJSBundle: (
entryPoint: string,
premodules: ReadonlyArray<Module>,
Expand All @@ -42,26 +85,29 @@ declare module 'metro/private/DeltaBundler/Serializers/baseJSBundle' {
export = baseJSBundle
}

declare module 'metro/private/lib/bundleToString' {
// https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/lib/bundleToString.js#L22
const baseJSBundle: (bundle: { modules: [number, string][]; post: string; pre: string }) => {
declare module 'metro/src/lib/bundleToString' {
import type { BundleMetadata } from 'metro'
const bundleToString: (bundle: { modules: [number, string][]; post: string; pre: string }) => {
code: string
metadata: BundleMetadata
}

export = baseJSBundle
export = bundleToString
}

declare module 'metro/private/lib/countLines' {
// https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/lib/countLines.js#L16
declare module 'metro/src/lib/countLines' {
const countLines: (code: string) => number
export = countLines
}

declare module 'metro/private/DeltaBundler/Serializers/sourceMapString' {
import type { MixedOutput, Module } from 'metro'
declare module 'metro/src/lib/CountingSet' {
class CountingSet<T> extends Set<T> {
constructor(items?: Iterable<T>)
}
export = CountingSet
}

// https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/Serializers/sourceMapString.js#L19
declare module 'metro/src/DeltaBundler/Serializers/sourceMapString' {
import type { MixedOutput, Module } from 'metro'
const sourceMapString: (
bundle: Module<MixedOutput>[],
options: {
Expand Down
42 changes: 12 additions & 30 deletions packages/react-native/src/tooling/vendor/metro/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,46 +26,29 @@

// eslint-disable-next-line import/no-extraneous-dependencies
import type { MixedOutput, Module, ReadOnlyGraph } from 'metro'
import type * as baseJSBundleType from 'metro/private/DeltaBundler/Serializers/baseJSBundle'
import type * as sourceMapStringType from 'metro/private/DeltaBundler/Serializers/sourceMapString'
import type * as bundleToStringType from 'metro/private/lib/bundleToString'
import type * as baseJSBundleType from 'metro/src/DeltaBundler/Serializers/baseJSBundle'
import type * as sourceMapStringType from 'metro/src/DeltaBundler/Serializers/sourceMapString'
import type * as bundleToStringType from 'metro/src/lib/bundleToString'
import type { MetroSerializer } from '../../utils'

let baseJSBundleModule: any
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
baseJSBundleModule = require('metro/private/DeltaBundler/Serializers/baseJSBundle')
} catch {
// eslint-disable-next-line @typescript-eslint/no-require-imports
baseJSBundleModule = require('metro/src/DeltaBundler/Serializers/baseJSBundle')
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const baseJSBundleModule = require('metro/src/DeltaBundler/Serializers/baseJSBundle')

const baseJSBundle: typeof baseJSBundleType =
typeof baseJSBundleModule === 'function'
? baseJSBundleModule
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(baseJSBundleModule?.baseJSBundle ?? baseJSBundleModule?.default)

let sourceMapString: typeof sourceMapStringType
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const sourceMapStringModule = require('metro/private/DeltaBundler/Serializers/sourceMapString')
sourceMapString = (sourceMapStringModule as { sourceMapString: typeof sourceMapStringType }).sourceMapString
} catch (e) {
sourceMapString = require('metro/src/DeltaBundler/Serializers/sourceMapString')
if ('sourceMapString' in sourceMapString) {
// Changed to named export in https://github.com/facebook/metro/commit/34148e61200a508923315fbe387b26d1da27bf4b
// Metro 0.81.0 and 0.80.10 patch
sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
let sourceMapString: typeof sourceMapStringType = require('metro/src/DeltaBundler/Serializers/sourceMapString')
if ('sourceMapString' in sourceMapString) {
// Changed to named export in https://github.com/facebook/metro/commit/34148e61200a508923315fbe387b26d1da27bf4b
sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString
}

let bundleToStringModule: any
try {
bundleToStringModule = require('metro/private/lib/bundleToString')
} catch {
bundleToStringModule = require('metro/src/lib/bundleToString')
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const bundleToStringModule = require('metro/src/lib/bundleToString')

const bundleToString: typeof bundleToStringType =
typeof bundleToStringModule === 'function'
Expand All @@ -74,7 +57,6 @@ const bundleToString: typeof bundleToStringType =
(bundleToStringModule?.bundleToString ?? bundleToStringModule?.default)

type NewSourceMapStringExport = {
// Since Metro v0.80.10 https://github.com/facebook/metro/compare/v0.80.9...v0.80.10#diff-1b836d1729e527a725305eef0cec22e44605af2700fa413f4c2489ea1a03aebcL28
sourceMapString: typeof sourceMapString
}

Expand Down
10 changes: 8 additions & 2 deletions packages/react-native/test/posthog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,10 @@ describe('PostHog React Native', () => {

await new Promise((resolve) => setTimeout(resolve, 100))

expect((globalThis as any).window.fetch).not.toHaveBeenCalled()
expect((globalThis as any).window.fetch).not.toHaveBeenCalledWith(
expect.stringContaining('/flags/'),
expect.any(Object)
)
})

it('should reload feature flags by default when calling setGroupPropertiesForFlags', async () => {
Expand All @@ -940,7 +943,10 @@ describe('PostHog React Native', () => {

await new Promise((resolve) => setTimeout(resolve, 100))

expect((globalThis as any).window.fetch).not.toHaveBeenCalled()
expect((globalThis as any).window.fetch).not.toHaveBeenCalledWith(
expect.stringContaining('/flags/'),
expect.any(Object)
)
})

it('should reload feature flags by default when calling resetPersonPropertiesForFlags', async () => {
Expand Down
44 changes: 32 additions & 12 deletions packages/react-native/test/storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,50 @@ describe('PostHog React Native', () => {

describe('storage', () => {
let storage: PostHogRNStorage

beforeEach(() => {
mockedOptionalFileSystem!.readAsStringAsync.mockImplementation(() => {
const res = Promise.resolve(
JSON.stringify({
version: 'v1',
content: {
foo: 'bar',
},
})
)
return res
jest.clearAllMocks()
})

it('should load storage from the file system', async () => {
// Use a deferred promise so we can control when it resolves
let resolveReadPromise: (value: string) => void
const readPromise = new Promise<string>((resolve) => {
resolveReadPromise = resolve
})

mockedOptionalFileSystem!.readAsStringAsync.mockReturnValue(readPromise)
storage = new PostHogRNStorage(buildOptimisiticAsyncStorage())
})

it('should load storage from the file system', async () => {
// Before promise resolves, value should be undefined
expect(storage.getItem('foo')).toEqual(undefined)

// Now resolve the promise
resolveReadPromise!(
JSON.stringify({
version: 'v1',
content: {
foo: 'bar',
},
})
)

await storage.preloadPromise
expect(mockedOptionalFileSystem!.readAsStringAsync).toHaveBeenCalledTimes(1)
expect(storage.getItem('foo')).toEqual('bar')
})

it('should save storage to the file system', async () => {
mockedOptionalFileSystem!.readAsStringAsync.mockResolvedValue(
JSON.stringify({
version: 'v1',
content: {},
})
)

storage = new PostHogRNStorage(buildOptimisiticAsyncStorage())
await storage.preloadPromise

storage.setItem('foo', 'bar2')
expect(storage.getItem('foo')).toEqual('bar2')
expect(mockedOptionalFileSystem!.writeAsStringAsync).toHaveBeenCalledWith(
Expand Down
8 changes: 7 additions & 1 deletion packages/react-native/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "@posthog-tooling/tsconfig-base",
"compilerOptions": {
"rootDir": "./src",
"baseUrl": ".",
"declaration": true,
"esModuleInterop": true,
"jsx": "react-native",
Expand All @@ -14,7 +15,12 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "ES2018"
"target": "ES2018",
"typeRoots": ["./node_modules/@types", "../../node_modules/@types"],
"paths": {
"react": ["./node_modules/@types/react"],
"react-native": ["./node_modules/react-native"]
}
},
"include": ["index.ts", "src/**/*.ts", "src/**/*.tsx"]
}
Loading
Loading