Expo 指纹

一个从 React Native 项目生成指纹的库。

Node

@expo/fingerprint 提供了一个 API 来生成项目的指纹(哈希),用于确定应用的原生层和 JavaScript 层之间的兼容性。哈希计算是可配置的,但默认情况下源自哈希应用依赖、自定义原生代码、原生项目文件和配置。

¥@expo/fingerprint provides an API to generate a fingerprint (hash) of your project for use in determining compatibility between the native layer and JavaScript layer of your app. The hash calculation is configurable, but is by default derived from hashing app dependencies, custom native code, native project files, and configuration.

安装

¥Installation

@expo/fingerprint 默认包含在 expoexpo-updates 中。

¥@expo/fingerprint is included with expo and expo-updates by default.

如果你希望将 @expo/fingerprint 用作独立包,则可以通过运行以下命令进行安装:

¥If you wish to use @expo/fingerprint as a standalone package, you can install it by running the command:

Terminal
npx expo install @expo/fingerprint

API 使用

¥API Usage

import * as Fingerprint from '@expo/fingerprint';

CLI 使用

¥CLI Usage

Terminal
npx @expo/fingerprint --help

配置

¥Configuration

@expo/fingerprint 提供的默认值应该适用于大多数项目,但也提供了几种配置指纹识别过程的方法,以更好地适应你的应用结构和工作流程。

¥@expo/fingerprint provides defaults that should work for most projects, but also provides a few ways to configure the fingerprinting process to better fit your app structure and workflow.

.fingerprintignore

.fingerprintignore 位于项目根目录中,是一种类似 .gitignore 的忽略机制,用于将文件排除在哈希计算之外。它的行为类似,但使用 minimatch 进行模式匹配,其中有一些 limitations(请参阅 选项ignorePaths 的文档)。

¥Placed in your project root, .fingerprintignore is a .gitignore-like ignore mechanism used to exclude files from hash calculation. It behaves similarly but instead uses minimatch for pattern matching which has some limitations (see documentation for ignorePaths under Options).

例如,要跳过文件夹但保留一些文件:

¥For example, to skip a folder but keep some files:

# Ignore the entire /app/ios folder
/app/ios/**/*

# But still keep /app/ios/Podfile and /app/ios/Podfile.lock
!/app/ios/Podfile
!/app/ios/Podfile.lock

fingerprint.config.js

fingerprint.config.js 位于你的项目根目录中,允许你指定自定义哈希计算配置,超出 .fingerprintignore 中可配置的内容。

¥Placed in your project root, fingerprint.config.js allows you to specify custom hash calculation configuration beyond what is configurable in the .fingerprintignore.

有关支持的配置,请参阅 配置SourceSkips

¥For supported configurations, see Config and SourceSkips.

以下是示例 finger.config.js 配置,假设你已将 @expo/fingerprint 安装为直接依赖:

¥Below is an example fingerprint.config.js configuration, assuming you have @expo/fingerprint installed as a direct dependency:

/** @type {import('@expo/fingerprint').Config} */
const config = {
  sourceSkips: [
    'ExpoConfigRuntimeVersionIfString',
    'ExpoConfigVersions',
    'PackageJsonAndroidAndIosScriptsIfNotContainRun',
  ],
};
module.exports = config;

如果你正在使用 @expo/fingerprintexpo(其中 @expo/fingerprint 作为传递依赖安装),则可以从 expo/fingerprint 导入指纹:

¥If you are using @expo/fingerprint through expo (where @expo/fingerprint is installed as a transitive dependency), you can import fingerprint from expo/fingerprint:

/** @type {import('expo/fingerprint').Config} */

局限性

¥Limitations

Limited support for @expo/config-plugins raw functions

使用具有原始功能的配置插件时,必须注意某些限制,特别是在指纹识别方面。库会尽最大努力为通过配置插件所做的更改生成指纹;但是,原始函数带来了特定的挑战。原始函数不能序列化为指纹,这意味着它们不能直接用于生成唯一哈希值。

¥When using config plugins with raw functions, it's essential to be aware of certain limitations, particularly in the context of fingerprinting. The library makes a best effort to generate fingerprints for changes made through config plugins; however, raw functions pose specific challenges. Raw functions are not serializable as fingerprints, which means they cannot be directly used for generating unique hashes.

为了解决此限制,库采用以下策略之一为原始函数创建可序列化的指纹:

¥To work around this limitation, the library employs one of the following strategies to create serializable fingerprints for raw functions:

  1. 使用 Function.name:如果命名原始函数可用,库将利用 Function.name 属性。此属性为该函数提供了一个可识别的名称,可用作指纹属性。

    ¥Using Function.name: The library utilizes the Function.name property if available for named raw functions. This property provides a recognizable name for the function, which can be used as a fingerprint property.

  2. 使用 withAnonymous:对于没有 Function.name 的匿名原始函数,库将使用 withAnonymous 作为指纹属性。这是匿名函数的通用标识符。

    ¥Using withAnonymous: For anonymous raw functions without a Function.name, the library resorts to using withAnonymous as the fingerprint property. This is a generic identifier for anonymous functions.

以下是说明库将使用 [withMyPlugin, withAnonymous] 作为指纹哈希插件属性的情况的示例:

¥Here's an example to illustrate a case in which the library will use [withMyPlugin, withAnonymous] as plugin properties for fingerprint hashing:

app.config.js
const { withInfoPlist } = require('expo/config-plugins');

const withMyPlugin = (config) => {
  return withInfoPlist(config, (config) => {
    config.modResults.NSLocationWhenInUseUsageDescription = 'Allow $(PRODUCT_NAME) to use your location';
    return config;
  });
};

export default ({ config }) => {
  config.plugins ||= [];
  config.plugins.push(withMyPlugin);
  config.plugins.push((config) => config);
  return config;
};

需要注意的是,由于这种设计,如果你对原始配置插件功能的实现进行更改,例如更改 withMyPlugin 中的 Info.plist 值,指纹仍将生成相同的哈希值。要在修改配置插件实现时确保唯一的指纹,请考虑以下选项:

¥It's important to note that due to this design, if you make changes to the implementation of raw config plugins functions, such as altering the Info.plist value within withMyPlugin, the fingerprint will still generate the same hash value. To ensure unique fingerprints when modifying config plugins implementations, consider the following options:

  • 避免匿名函数:避免使用匿名原始配置插件函数。相反,尽可能使用命名函数,并确保只要实现发生变化,它们的名称就保持一致。

    ¥Avoid Anonymous Functions: Avoid using anonymous raw config plugins functions. Instead, use named functions whenever possible, and ensure that their names remain consistent as long as the implementation changes.

  • 使用本地配置插件:或者,你可以将本地配置插件创建为单独的模块,每个模块都有自己的导出。此方法允许你在更改配置插件实现时指定不同的函数名称。

    ¥Use Local config plugins: Alternatively, you can create local config plugins as separate modules, each with its own export. This approach allows you to specify a different function name when making changes to the config plugins implementations.

以下是使用本地配置插件的示例:

¥Here's an example of using a local config plugin:

./plugins/withMyPlugin.js
const { withInfoPlist } = require('expo/config-plugins');

const withMyPlugin = config => {
  return withInfoPlist(config, config => {
    config.modResults.NSLocationWhenInUseUsageDescription =
      'Allow $(PRODUCT_NAME) to use your location';
    return config;
  });
};

module.exports = withMyPlugin;
app.json
{
  "expo": {
    %%placeholder-start%%... %%placeholder-end%%
    "plugins": "./plugins/withMyPlugin"
  }
}

通过遵循这些准则,你可以有效地管理对配置插件的更改并确保指纹识别保持一致和可靠。

¥By following these guidelines, you can effectively manage changes to config plugins and ensure that fingerprinting remains consistent and reliable.

Constants

Fingerprint.DEFAULT_IGNORE_PATHS

Node

Type: string[]

Fingerprint.DEFAULT_SOURCE_SKIPS

Node

Type: PackageJsonAndroidAndIosScriptsIfNotContainRun

Methods

Fingerprint.createFingerprintAsync(projectRoot, options)

Node
ParameterType
projectRootstring
options(optional)Options

Create a fingerprint for a project.

Example

const fingerprint = await createFingerprintAsync('/app');
console.log(fingerprint);

Fingerprint.createProjectHashAsync(projectRoot, options)

Node
ParameterType
projectRootstring
options(optional)Options

Create a native hash value for a project.

Returns:
Promise<string>

Example

const hash = await createProjectHashAsync('/app');
console.log(hash);

Fingerprint.diffFingerprintChangesAsync(fingerprint, projectRoot, options)

Node
ParameterType
fingerprintFingerprint
projectRootstring
options(optional)Options

Diff the fingerprint with the fingerprint of the provided project.

Example

// Create a fingerprint for the project
const fingerprint = await createFingerprintAsync('/app');

// Make some changes to the project

// Calculate the diff
const diff = await diffFingerprintChangesAsync(fingerprint, '/app');
console.log(diff);

Fingerprint.diffFingerprints(fingerprint1, fingerprint2)

Node
ParameterType
fingerprint1Fingerprint
fingerprint2Fingerprint

Diff two fingerprints. The implementation assumes that the sources are sorted.

Example

// Create a fingerprint for the project
const fingerprint = await createFingerprintAsync('/app');

// Make some changes to the project

// Create a fingerprint again
const fingerprint2 = await createFingerprintAsync('/app');
const diff = await diffFingerprints(fingerprint, fingerprint2);
console.log(diff);

Interfaces

DebugInfoContents

Node
PropertyTypeDescription
hashstring-
isTransformed(optional)boolean

Indicates whether the source is transformed by fileHookTransform.

DebugInfoDir

Node
PropertyTypeDescription
children(undefined | DebugInfoFile | DebugInfoDir)[]-
hashstring-
pathstring-

DebugInfoFile

Node
PropertyTypeDescription
hashstring-
isTransformed(optional)boolean

Indicates whether the source is transformed by fileHookTransform.

pathstring-

Fingerprint

Node
PropertyTypeDescription
hashstring

The final hash value of the whole project fingerprint.

sourcesFingerprintSource[]

Sources and their hash values from which the project fingerprint was generated.

HashResultContents

Node
PropertyTypeDescription
debugInfo(optional)DebugInfoContents-
hexstring-
idstring-
type'contents'-

HashResultDir

Node
PropertyTypeDescription
debugInfo(optional)DebugInfoDir-
hexstring-
idstring-
type'dir'-

HashResultFile

Node
PropertyTypeDescription
debugInfo(optional)DebugInfoFile-
hexstring-
idstring-
type'file'-

HashSourceContents

Node
PropertyTypeDescription
contentsstring | Buffer-
idstring-
reasonsstring[]

Reasons of this source coming from.

type'contents'-

HashSourceDir

Node
PropertyTypeDescription
filePathstring-
reasonsstring[]

Reasons of this source coming from.

type'dir'-

HashSourceFile

Node
PropertyTypeDescription
filePathstring-
reasonsstring[]

Reasons of this source coming from.

type'file'-

Options

Node
PropertyTypeDescription
concurrentIoLimit(optional)number

I/O concurrency limit.

Default:The number of CPU cores.
debug(optional)boolean

Whether to include verbose debug info in source output. Useful for debugging.

dirExcludes(optional)string[]

Deprecated Use ignorePaths instead.

Exclude specified directories from hashing. The supported pattern is the same as glob(). Default is ['android/build', 'android/app/build', 'android/app/.cxx', 'ios/Pods'].

enableReactImportsPatcher(optional)boolean

Enable ReactImportsPatcher to transform imports from React of the form #import "RCTBridge.h" to #import <React/RCTBridge.h>. This is useful when you want to have a stable fingerprint for Expo projects, since expo-modules-autolinking will change the import style on iOS.

Default:true for Expo SDK 51 and lower.
extraSources(optional)HashSource[]

Additional sources for hashing.

fileHookTransform(optional)FileHookTransformFunction

A custom hook function to transform file content sources before hashing.

hashAlgorithm(optional)string

The algorithm to use for crypto.createHash().

Default:'sha1'
ignorePaths(optional)string[]

Ignore files and directories from hashing. The supported pattern is the same as glob().

Please note that the pattern matching is slightly different from gitignore. Partial matching is unsupported. For example, build does not match android/build; instead, use '**' + '/build'.

See: minimatch implementations for further reference.

Fingerprint comes with implicit default ignorePaths defined in Options.DEFAULT_IGNORE_PATHS. If you want to override the default ignorePaths, use ! prefix in ignorePaths.

platforms(optional)Platform[]

Limit native files to those for specified platforms.

Default:['android', 'ios']
silent(optional)boolean

Whether running the functions should mute all console output. This is useful when fingerprinting is being done as part of a CLI that outputs a fingerprint and outputting anything else pollutes the results.

sourceSkips(optional)SourceSkips

Skips some sources from fingerprint. Value is the result of bitwise-OR'ing desired values of SourceSkips.

Default:DEFAULT_SOURCE_SKIPS
useRNCoreAutolinkingFromExpo(optional)boolean

Use the react-native core autolinking sources from expo-modules-autolinking rather than @react-native-community/cli.

Default:true for Expo SDK 52 and higher.

Types

Config

Node

Supported options for use in fingerprint.config.js

Type: Pick<Options, 'concurrentIoLimit' | 'hashAlgorithm' | 'ignorePaths' | 'extraSources' | 'enableReactImportsPatcher' | 'useRNCoreAutolinkingFromExpo' | 'debug' | 'fileHookTransform'> extended by:

PropertyTypeDescription
sourceSkips(optional)SourceSkips | SourceSkipsKeys[]-

DebugInfo

Node

Literal Type: multiple types

Acceptable values are: DebugInfoFile | DebugInfoDir | DebugInfoContents

FileHookTransformFunction(source, chunk, isEndOfFile, encoding)

Node

Hook function to transform file content sources before hashing.

ParameterType
sourceFileHookTransformSource
chunkBuffer | string | null
isEndOfFileboolean
encodingBufferEncoding
Returns:

Buffer | string | null

FileHookTransformSource

Node

The source parameter for FileHookTransformFunction.

Type: object shaped as below:

PropertyTypeDescription
filePathstring-
type'file'-

Or object shaped as below:

PropertyTypeDescription
idstring-
type'contents'-

FingerprintDiffItem

Node

Type: object shaped as below:

PropertyTypeDescription
addedSourceFingerprintSource

The added source.

op'added'

The operation type of the diff item.

Or object shaped as below:

PropertyTypeDescription
op'removed'

The operation type of the diff item.

removedSourceFingerprintSource

The removed source.

Or object shaped as below:

PropertyTypeDescription
afterSourceFingerprintSource

The source after.

beforeSourceFingerprintSource

The source before.

op'changed'

The operation type of the diff item.

FingerprintSource

Node

Type: HashSource extended by:

PropertyTypeDescription
debugInfo(optional)DebugInfo

Debug info from the hashing process. Differs based on source type. Designed to be consumed by humans as opposed to programmatically.

hashstring | null

Hash value of the source. If the source is excluded the value will be null.

HashResult

Node

Literal Type: multiple types

Acceptable values are: HashResultFile | HashResultDir | HashResultContents

HashSource

Node

Literal Type: multiple types

Acceptable values are: HashSourceFile | HashSourceDir | HashSourceContents

Platform

Node

Literal Type: string

Acceptable values are: 'android' | 'ios'

Enums

SourceSkips

Node

Bitmask of values that can be used to skip certain parts of the sourcers when generating a fingerprint.

None

SourceSkips.None = 0

Skip nothing

ExpoConfigVersions

SourceSkips.ExpoConfigVersions = 1

Versions in app.json, including Android versionCode and iOS buildNumber

ExpoConfigRuntimeVersionIfString

SourceSkips.ExpoConfigRuntimeVersionIfString = 2

runtimeVersion in app.json if it is a string

ExpoConfigNames

SourceSkips.ExpoConfigNames = 4

App names in app.json, including shortName and description

ExpoConfigAndroidPackage

SourceSkips.ExpoConfigAndroidPackage = 8

Android package name in app.json

ExpoConfigIosBundleIdentifier

SourceSkips.ExpoConfigIosBundleIdentifier = 16

iOS bundle identifier in app.json

ExpoConfigSchemes

SourceSkips.ExpoConfigSchemes = 32

Schemes in app.json

ExpoConfigEASProject

SourceSkips.ExpoConfigEASProject = 64

EAS project information in app.json

ExpoConfigAssets

SourceSkips.ExpoConfigAssets = 128

Assets in app.json, including icons and splash assets

ExpoConfigAll

SourceSkips.ExpoConfigAll = 256

Skip the whole ExpoConfig. Prefer the other ExpoConfig source skips when possible and use this flag with caution. This will potentially ignore some native changes that should be part of most fingerprints. E.g., adding a new config plugin, changing the app icon, or changing the app name.

PackageJsonAndroidAndIosScriptsIfNotContainRun

SourceSkips.PackageJsonAndroidAndIosScriptsIfNotContainRun = 512

package.json scripts if android and ios items do not contain "run". Because prebuild will change the scripts in package.json, this is useful to generate a consistent fingerprint before and after prebuild.

PackageJsonScriptsAll

SourceSkips.PackageJsonScriptsAll = 1024

Skip the whole scripts section in the project's package.json.

GitIgnore

SourceSkips.GitIgnore = 2048

Skip .gitignore files.