Reference version

Expo Expo 指纹 iconExpo 指纹

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

Node
Bundled version:
~0.13.4

For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

@expo/fingerprint 提供了一个 API,用于生成你的项目指纹(哈希),以便用于判断应用的原生层和 JavaScript 层之间的兼容性。哈希计算是可配置的,但默认情况下会通过对应用依赖项、自定义原生代码、原生项目文件和配置进行哈希处理来生成。

安装

@expo/fingerprint 默认随 expoexpo-updates 一起包含。

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

Terminal
npx expo install @expo/fingerprint

CLI 用法

Terminal
npx @expo/fingerprint --help

配置

@expo/fingerprint 提供了适用于大多数项目的默认配置,但也提供了几种方式来自定义指纹生成过程,使其更适合你的应用结构和工作流程。

.fingerprintignore

放置在项目根目录中的 .fingerprintignore 是一种类似 .gitignore 的忽略机制,用于排除不参与哈希计算的文件。所有模式路径都相对于项目根目录。它的行为类似,但实际上使用 minimatch 进行模式匹配,这有一些 限制(有关 OptionsignorePaths 的说明请参见文档)。

下面是一个 .fingerprintignore 配置示例:

.fingerprintignore
# 忽略整个 android 目录 android/**/* # 忽略整个 ios 目录,但仍保留 ios/Podfile 和 ios/Podfile.lock ios/**/* !ios/Podfile !ios/Podfile.lock # 忽略 node_modules 中的特定包 node_modules/some-package/**/* # 与上面相同,但作用范围更广,因为包可能嵌套存在 **/node_modules/some-package/**/*

fingerprint.config.js

放置在项目根目录中的 fingerprint.config.js 允许你在 .fingerprintignore 可配置的范围之外,指定自定义的哈希计算配置。有关受支持的配置,请参见 ConfigSourceSkips

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

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

如果你通过 expo 使用 @expo/fingerprint(此时 @expo/fingerprint 作为传递依赖安装),你可以从 expo/fingerprint 导入 fingerprint:

/** @type {import('expo/fingerprint').Config} */
高级:在指纹哈希之前自定义源码

在某些情况下,你可能希望在生成指纹之前自定义源码。例如:

  • 你想从应用配置中移除敏感数据。
  • 你想稳定应用配置中的动态值。
  • 你想将文件哈希转换为稳定值。

为此,你可以在 fingerprint.config.js 文件中使用 fileHookTransform 选项,在哈希之前转换源码。了解更多关于 fileHookTransform 选项 的信息。

fingerprint.config.js
const assert = require('node:assert'); const fileChunkMap = {}; /** @type {import('@expo/fingerprint').Config} */ const config = { fileHookTransform: (source, chunk, isEndOfFile, encoding) => { // 从应用配置中移除 "updates" 部分 if (source.type === 'contents' && source.id === 'expoConfig') { assert(isEndOfFile, 'contents source is expected to have single chunk.'); const config = JSON.parse(chunk); delete config.updates; return JSON.stringify(config); } // 将内容源转换为空字符串 if (source.type === 'contents' && source.id === 'packageJson:scripts') { return ''; } // 通过替换动态值来转换文件源 if (source.type === 'file' && source.filePath === 'eas.json') { return chunk.toString().replace(/MyApp-Dev/g, 'MyApp'); } // 转换一个分多块处理的大文件 // 要获取完整文件,需要缓存所有块并一次性返回 if (source.type === 'file' && source.filePath === 'assets/large-image.jpg') { let receivedBuffer = fileChunkMap[source.filePath] ?? Buffer.alloc(0); if (chunk != null) { const buffer = typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk; receivedBuffer = Buffer.concat([receivedBuffer, buffer]); fileChunkMap[source.filePath] = receivedBuffer; } if (!isEndOfFile) { return null; } fileChunkMap[source.filePath] = null; // 完整载荷可在此处获取,你可以根据需要进行转换。 receivedBuffer = receivedBuffer.toString().replace(/SensitiveData/g, 'StableData'); return receivedBuffer; } // 对于其他源,直接返回 chunk return chunk; }, }; module.exports = config;

限制

@expo/config-plugins 原始函数的支持有限

在使用带有原始函数的配置插件时,必须注意某些限制,尤其是在指纹生成的上下文中。该库会尽最大努力为通过配置插件所做的更改生成指纹;然而,原始函数带来了特定挑战。原始函数无法作为指纹被序列化,这意味着它们不能直接用于生成唯一哈希。

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

  1. 使用 Function.name:对于具名的原始函数,如果可用,库会使用 Function.name 属性。该属性会提供函数一个可识别的名称,可作为指纹属性使用。

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

下面是一个示例,用于说明库将在何种情况下使用 [withMyPlugin, withAnonymous] 作为插件属性来进行指纹哈希:

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 值,指纹仍然会生成相同的哈希值。为了在修改配置插件实现时确保指纹唯一,请考虑以下选项:

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

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

下面是一个使用本地配置插件的示例:

./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" } }

遵循这些准则后,你可以有效管理对配置插件的更改,并确保指纹生成保持一致且可靠。

API

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

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: union

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: union

Acceptable values are: HashResultFile | HashResultDir | HashResultContents

HashSource

Node

Literal Type: union

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.

ExpoConfigExtraSection

SourceSkips.ExpoConfigExtraSection = 4096

The extra section in app.json