Expo 指纹
一个从 React Native 项目生成指纹的库。
@expo/fingerprint 提供了一个 API,用于生成项目的指纹(哈希),以用于确定你的应用的原生层与 JavaScript 层之间的兼容性。哈希计算是可配置的,但默认情况下是通过对应用依赖、自定义原生代码、原生项目文件和配置进行哈希生成的。
安装
🌐 Installation
@expo/fingerprint 默认包含在 expo 和 expo-updates 中。
如果你希望将 @expo/fingerprint 用作独立软件包,可以通过运行以下命令进行安装:
🌐 If you wish to use @expo/fingerprint as a standalone package, you can install it by running the command:
- npx expo install @expo/fingerprintCLI 使用
🌐 CLI Usage
- npx @expo/fingerprint --help配置
🌐 Configuration
@expo/fingerprint 提供了适用于大多数项目的默认设置,同时也提供了一些方法来配置指纹识别过程,以更好地适应你的应用结构和工作流程。
.fingerprintignore
放置在项目根目录下的 .fingerprintignore 是一个类似于 .gitignore 的忽略机制,用于在哈希计算中排除文件。所有模式路径都是相对于项目根目录的。它的行为类似,但使用 minimatch 进行模式匹配,这有一些 限制(有关 ignorePaths 的详细信息,请参见 选项 下的文档)。
🌐 Placed in your project root, .fingerprintignore is a .gitignore-like ignore mechanism used to exclude files from hash calculation. All pattern paths are relative to the project root. It behaves similarly but instead uses minimatch for pattern matching which has some limitations (see documentation for ignorePaths under Options).
这是一个 .fingerprintignore 配置示例:
🌐 Here is an example .fingerprintignore configuration:
# Ignore the entire android directory android/**/* # Ignore the entire ios directory but still keep ios/Podfile and ios/Podfile.lock ios/**/* !ios/Podfile !ios/Podfile.lock # Ignore specific package in node_modules node_modules/some-package/**/* # Same as above but having broader scope because packages may be nested **/node_modules/some-package/**/*
fingerprint.config.js
放置在项目根目录下的 fingerprint.config.js 允许你指定自定义的哈希计算配置,这些配置超出了 .fingerprintignore 能设置的范围。有关支持的配置,请参见 Config 和 SourceSkips。
🌐 Placed in your project root, fingerprint.config.js allows you to specify custom hash calculation configuration beyond what is configurable in the .fingerprintignore. For supported configurations, see Config and SourceSkips.
下面是一个 fingerprint.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/fingerprint 到 expo(其中 @expo/fingerprint 作为传递依赖安装),你可以从 expo/fingerprint 导入 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} */
高级:在指纹哈希前自定义来源
在某些情况下,你可能希望在进行指纹识别之前自定义来源。例如:
🌐 In some cases, you may want to customize the sources before fingerprinting. For example:
- 你想从应用配置中删除敏感数据。
- 你想在应用配置中稳定动态值。
- 你想将文件哈希转换为稳定的值。
为此,你可以在 fingerprint.config.js 文件中使用 fileHookTransform 选项在哈希之前转换源。了解有关 fileHookTransform 选项 的更多信息。
🌐 To do this, you can use the fileHookTransform option in the fingerprint.config.js file to transform the sources before hashing. Learn more about the fileHookTransform option.
const assert = require('node:assert'); const fileChunkMap = {}; /** @type {import('@expo/fingerprint').Config} */ const config = { fileHookTransform: (source, chunk, isEndOfFile, encoding) => { // Remove the "updates" section from the app config 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); } // Transform content sources to an empty string if (source.type === 'contents' && source.id === 'packageJson:scripts') { return ''; } // Transform a file source by replacing dynamic values if (source.type === 'file' && source.filePath === 'eas.json') { return chunk.toString().replace(/MyApp-Dev/g, 'MyApp'); } // Transform a large file that is processed in multiple chunks // To get the full file, buffer all chunks and return them all at once 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; // The full payload is available here and you can transform it as needed. receivedBuffer = receivedBuffer.toString().replace(/SensitiveData/g, 'StableData'); return receivedBuffer; } // For other sources, just return the chunk return chunk; }, }; module.exports = config;
局限性
🌐 Limitations
对 @expo/config-plugins 原始函数的支持有限
在使用带有原始函数的配置插件时,必须注意某些限制,特别是在指纹识别的上下文中。该库会尽力为通过配置插件所做的更改生成指纹;然而,原始函数会带来特定的挑战。原始函数无法序列化为指纹,这意味着它们不能直接用于生成唯一的哈希值。
🌐 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:
- 使用
Function.name:该库在可用时会使用Function.name属性来命名原始函数。此属性为函数提供了可识别的名称,可用于作为指纹属性。 - 使用
withAnonymous:对于没有Function.name的匿名原始函数,库会使用withAnonymous作为指纹属性。这是匿名函数的通用标识符。
下面是一个示例,说明在某种情况下库将使用 [withMyPlugin, withAnonymous] 作为指纹哈希的插件属性:
🌐 Here's an example to illustrate a case in which the library will use [withMyPlugin, withAnonymous] as plugin properties for fingerprint hashing:
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:
- 避免使用匿名函数:避免使用匿名的原始配置插件函数。相反,应尽可能使用具名函数,并确保在实现更改时函数名称保持一致。
- 使用本地配置插件:或者,你可以将本地配置插件创建为独立模块,每个模块都有自己的导出。此方法允许你在修改配置插件实现时指定不同的函数名称。
这是使用本地配置插件的一个示例:
🌐 Here's an example of using a local config plugin:
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;
{ "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.
应用接口
🌐 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 | (undefined | DebugInfoFile | DebugInfoDir)[] | - |
| 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 | - |
| id | string | - |
| reasons | string[] | Reasons of this source coming from. |
| type | 'contents' | - |
| Property | Type | Description |
|---|---|---|
| filePath | string | - |
| reasons | string[] | Reasons of this source coming from. |
| type | 'dir' | - |
| Property | Type | Description |
|---|---|---|
| filePath | string | - |
| 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[] | - |
Literal Type: union
Acceptable values are: DebugInfoFile | DebugInfoDir | DebugInfoContents
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 sourcers when generating a fingerprint.
SourceSkips.ExpoConfigVersions = 1Versions in app.json, including Android versionCode and iOS buildNumber
SourceSkips.ExpoConfigRuntimeVersionIfString = 2runtimeVersion in app.json if it is a string
SourceSkips.ExpoConfigNames = 4App names in app.json, including shortName and 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.