This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
Expo 指纹
一个用于从 React Native 项目生成指纹的库。
@expo/fingerprint 提供了一个 API,用于为你的项目生成指纹(哈希),以便判断应用的原生层与 JavaScript 层之间的兼容性。哈希计算是可配置的,但默认会通过对应用依赖项、自定义原生代码、原生项目文件以及配置进行哈希来生成。
安装
@expo/fingerprint 默认包含在 expo 和 expo-updates 中。
如果你希望将 @expo/fingerprint 作为独立包使用,可以通过运行以下命令进行安装:
- npx expo install @expo/fingerprintCLI 用法
- npx @expo/fingerprint --help配置
@expo/fingerprint 提供了适用于大多数项目的默认配置,同时也提供了一些方式来配置指纹生成流程,以便更好地适配你的应用结构和工作流。
.fingerprintignore
放置在项目根目录中的 .fingerprintignore 是一种类似 .gitignore 的忽略机制,用于将文件排除在哈希计算之外。所有模式路径都相对于项目根目录。它的行为类似,但使用的是 minimatch 进行模式匹配,并且存在一些 限制(请参阅 Options 下 ignorePaths 的文档)。
下面是一个 .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 可配置范围的自定义哈希计算配置。关于支持的配置,请参阅 Config 和 SourceSkips。
下面是一个 fingerprint.config.js 配置示例,假设你已将 @expo/fingerprint 作为直接依赖安装:
/** @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} */
高级:在指纹哈希之前自定义 sources
在某些情况下,你可能希望在生成指纹之前自定义 sources。例如:
- 你想从应用配置中移除敏感数据。
- 你想稳定应用配置中的动态值。
- 你想将文件哈希转换为稳定值。
为此,你可以在 fingerprint.config.js 文件中使用 fileHookTransform 选项,在哈希计算之前转换 sources。了解更多关于 fileHookTransform 选项 的信息。
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; } // 对于其他 sources,直接返回 chunk return chunk; }, }; module.exports = config;
限制
对 @expo/config-plugins 原始函数的支持有限
在使用带有原始函数的配置插件时,必须了解某些限制,尤其是在指纹识别(fingerprinting)的上下文中。该库会尽最大努力为通过配置插件所做的更改生成指纹;然而,原始函数会带来特定挑战。原始函数无法序列化为指纹,这意味着它们不能直接用于生成唯一哈希。
为了绕过这一限制,该库会采用以下策略之一为原始函数创建可序列化的指纹:
-
使用
Function.name:如果命名的原始函数可用,库会利用Function.name属性。该属性提供了函数的可识别名称,可用作指纹属性。 -
使用
withAnonymous:对于没有Function.name的匿名原始函数,库会使用withAnonymous作为指纹属性。这是匿名函数的通用标识符。
下面是一个示例,用于说明库会如何将 [withMyPlugin, withAnonymous] 作为插件属性来进行指纹哈希:
const { withInfoPlist } = require('expo/config-plugins'); const withMyPlugin = (config) => { return withInfoPlist(config, (config) => { config.modResults.NSLocationWhenInUseUsageDescription = '允许 $(PRODUCT_NAME) 使用您的位置'; return config; }); }; export default ({ config }) => { config.plugins ||= []; config.plugins.push(withMyPlugin); config.plugins.push((config) => config); return config; };
需要注意的是,由于这种设计,如果你对原始配置插件函数的实现进行了更改,例如修改 withMyPlugin 中的 Info.plist 值,指纹仍然会生成相同的哈希值。为了在修改配置插件实现时确保指纹唯一,请考虑以下选项:
-
避免匿名函数:避免使用匿名的原始配置插件函数。相反,尽可能使用命名函数,并确保只要实现发生变化,它们的名称也保持一致。
-
使用本地配置插件:或者,你可以将本地配置插件创建为独立模块,每个模块都有自己的导出。这样在修改配置插件实现时,你可以指定不同的函数名称。
下面是使用本地配置插件的示例:
const { withInfoPlist } = require('expo/config-plugins'); const withMyPlugin = config => { return withInfoPlist(config, config => { config.modResults.NSLocationWhenInUseUsageDescription = '允许 $(PRODUCT_NAME) 使用您的位置'; return config; }); }; module.exports = withMyPlugin;
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": "./plugins/withMyPlugin" } }
通过遵循这些准则,你可以有效管理对配置插件的更改,并确保指纹识别保持一致且可靠。
API
import * as Fingerprint from '@expo/fingerprint';
Constants
Methods
| Parameter | Type |
|---|---|
| projectRoot | string |
| options(optional) | Options |
Create a fingerprint for a project.
Promise<Fingerprint>Example
const fingerprint = await createFingerprintAsync('/app'); console.log(fingerprint);
| Parameter | Type |
|---|---|
| projectRoot | string |
| options(optional) | Options |
Create a native hash value for a project.
Promise<string>Example
const hash = await createProjectHashAsync('/app'); console.log(hash);
| Parameter | Type |
|---|---|
| fingerprint | Fingerprint |
| projectRoot | string |
| options(optional) | Options |
Diff the fingerprint with the fingerprint of the provided project.
Promise<FingerprintDiffItem[]>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);
| Parameter | Type |
|---|---|
| fingerprint1 | Fingerprint |
| fingerprint2 | Fingerprint |
Diff two fingerprints. The implementation assumes that the sources are sorted.
FingerprintDiffItem[]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
| Property | Type | Description |
|---|---|---|
| hash | string | - |
| isTransformed(optional) | boolean | Indicates whether the source is transformed by |
| Property | Type | Description |
|---|---|---|
| children | (DebugInfoFile | DebugInfoDir | undefined)[] | - |
| hash | string | - |
| path | string | - |
| Property | Type | Description |
|---|---|---|
| hash | string | - |
| isTransformed(optional) | boolean | Indicates whether the source is transformed by |
| path | string | - |
| Property | Type | Description |
|---|---|---|
| hash | string | The final hash value of the whole project fingerprint. |
| sources | FingerprintSource[] | Sources and their hash values from which the project fingerprint was generated. |
| Property | Type | Description |
|---|---|---|
| debugInfo(optional) | DebugInfoContents | - |
| hex | string | - |
| id | string | - |
| type | 'contents' | - |
| Property | Type | Description |
|---|---|---|
| debugInfo(optional) | DebugInfoDir | - |
| hex | string | - |
| id | string | - |
| type | 'dir' | - |
| Property | Type | Description |
|---|---|---|
| debugInfo(optional) | DebugInfoFile | - |
| hex | string | - |
| id | string | - |
| type | 'file' | - |
| Property | Type | Description |
|---|---|---|
| contents | string | Buffer<ArrayBufferLike> | - |
| id | string | - |
| reasons | string[] | Reasons of this source coming from. |
| type | 'contents' | - |
| Property | Type | Description |
|---|---|---|
| filePath | string | - |
| overrideHashKey(optional) | string | Override key for hashing.
Without this key, the |
| reasons | string[] | Reasons of this source coming from. |
| type | 'dir' | - |
| Property | Type | Description |
|---|---|---|
| filePath | string | - |
| overrideHashKey(optional) | string | Override key for hashing.
Without this key, the |
| reasons | string[] | Reasons of this source coming from. |
| type | 'file' | - |
| Property | Type | Description |
|---|---|---|
| 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[] |
Exclude specified directories from hashing. The supported pattern is the same as |
| enableReactImportsPatcher(optional) | boolean | Enable ReactImportsPatcher to transform imports from React of the form 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 Default: 'sha1' |
| ignorePaths(optional) | string[] | Ignore files and directories from hashing. The supported pattern is the same as The pattern matching is slightly different from gitignore. Partial matching is unsupported. For example,
|
| 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 Default: true for Expo SDK 52 and higher. |
Types
Supported options for use in fingerprint.config.js
Type: Pick<Options, 'concurrentIoLimit' | 'hashAlgorithm' | 'ignorePaths' | 'extraSources' | 'enableReactImportsPatcher' | 'useRNCoreAutolinkingFromExpo' | 'debug' | 'fileHookTransform'> extended by:
| Property | Type | Description |
|---|---|---|
| sourceSkips(optional) | SourceSkips | SourceSkipsKeys[] | - |
Hook function to transform file content sources before hashing.
| Parameter | Type |
|---|---|
| source | FileHookTransformSource |
| chunk | Buffer | string | null |
| isEndOfFile | boolean |
| encoding | BufferEncoding |
Buffer | string | null
The source parameter for FileHookTransformFunction.
Type: object shaped as below:
| Property | Type | Description |
|---|---|---|
| filePath | string | - |
| type | 'file' | - |
Or object shaped as below:
| Property | Type | Description |
|---|---|---|
| id | string | - |
| type | 'contents' | - |
Type: object shaped as below:
| Property | Type | Description |
|---|---|---|
| addedSource | FingerprintSource | The added source. |
| op | 'added' | The operation type of the diff item. |
Or object shaped as below:
| Property | Type | Description |
|---|---|---|
| op | 'removed' | The operation type of the diff item. |
| removedSource | FingerprintSource | The removed source. |
Or object shaped as below:
| Property | Type | Description |
|---|---|---|
| afterSource | FingerprintSource | The source after. |
| beforeSource | FingerprintSource | The source before. |
| op | 'changed' | The operation type of the diff item. |
Type: HashSource extended by:
| Property | Type | Description |
|---|---|---|
| debugInfo(optional) | DebugInfo | Debug info from the hashing process. Differs based on source type. Designed to be consumed by humans as opposed to programmatically. |
| hash | string | null | Hash value of the |
Literal type: union
Acceptable values are: HashResultFile | HashResultDir | HashResultContents
Literal type: union
Acceptable values are: HashSourceFile | HashSourceDir | HashSourceContents
Enums
Bitmask of values that can be used to skip certain parts of the sources when generating a fingerprint.
SourceSkips.ExpoConfigVersions = 1Versions in app.json, including version, android.versionCode, and ios.buildNumber.
SourceSkips.ExpoConfigRuntimeVersionIfString = 2runtimeVersion in app.json if it is a string.
SourceSkips.ExpoConfigNames = 4App names in app.json, including name, description, web.name, web.shortName, and web.description.
SourceSkips.ExpoConfigIosBundleIdentifier = 16iOS bundle identifier in app.json.
SourceSkips.ExpoConfigAssets = 128Assets in app.json, including icons and splash assets.
SourceSkips.ExpoConfigAll = 256Skip 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.
SourceSkips.PackageJsonAndroidAndIosScriptsIfNotContainRun = 512package.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.
SourceSkips.PackageJsonScriptsAll = 1024Skip the whole scripts section in the project's package.json.