@expo/fingerprint 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/bin/cli.js +26 -5
- package/build/sourcer/Bare.js +13 -7
- package/build/sourcer/Bare.js.map +1 -1
- package/package.json +16 -9
- package/.eslintignore +0 -1
- package/CHANGELOG.md +0 -55
- package/__mocks__/@expo/spawn-async.ts +0 -30
- package/__mocks__/fs/promises.ts +0 -17
- package/__mocks__/fs.ts +0 -2
- package/__mocks__/resolve-from.ts +0 -24
- package/babel.config.js +0 -6
- package/e2e/__tests__/__snapshots__/managed-test.ts.snap +0 -242
- package/e2e/__tests__/bare-test.ts +0 -72
- package/e2e/__tests__/managed-test.ts +0 -165
- package/e2e/jest.config.js +0 -11
- package/jest.config.js +0 -10
- package/scripts/createFixture.ts +0 -81
- package/src/Dedup.ts +0 -97
- package/src/Fingerprint.ts +0 -60
- package/src/Fingerprint.types.ts +0 -110
- package/src/Options.ts +0 -81
- package/src/Sort.ts +0 -22
- package/src/__tests__/Dedup-test.ts +0 -177
- package/src/__tests__/Fingerprint-test.ts +0 -143
- package/src/__tests__/Sort-test.ts +0 -56
- package/src/hash/Hash.ts +0 -203
- package/src/hash/__tests__/Hash-test.ts +0 -238
- package/src/index.ts +0 -2
- package/src/sourcer/Bare.ts +0 -115
- package/src/sourcer/Expo.ts +0 -223
- package/src/sourcer/ExpoConfigLoader.ts +0 -84
- package/src/sourcer/PatchPackage.ts +0 -18
- package/src/sourcer/Sourcer.ts +0 -58
- package/src/sourcer/Utils.ts +0 -62
- package/src/sourcer/__tests__/Bare-test.ts +0 -88
- package/src/sourcer/__tests__/Expo-test.ts +0 -305
- package/src/sourcer/__tests__/PatchPackage-test.ts +0 -57
- package/src/sourcer/__tests__/Sourcer-test.ts +0 -21
- package/src/sourcer/__tests__/Utils-test.ts +0 -41
- package/src/sourcer/__tests__/__snapshots__/Bare-test.ts.snap +0 -21
- package/src/sourcer/__tests__/__snapshots__/Expo-test.ts.snap +0 -139
- package/src/sourcer/__tests__/fixtures/BareReactNative70Project.json +0 -47
- package/src/sourcer/__tests__/fixtures/ExpoAutolinkingAndroid.json +0 -82
- package/src/sourcer/__tests__/fixtures/ExpoAutolinkingIos.json +0 -114
- package/src/sourcer/__tests__/fixtures/ExpoManaged47Project.json +0 -6
- package/src/sourcer/__tests__/fixtures/PatchPackage.json +0 -4
- package/src/sourcer/__tests__/fixtures/RncliAutoLinking.json +0 -165
- package/src/utils/Path.ts +0 -26
- package/src/utils/Profile.ts +0 -47
- package/src/utils/__tests__/Path-test.ts +0 -36
- package/src/utils/__tests__/Profile-test.ts +0 -11
- package/tsconfig.json +0 -9
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import spawnAsync from '@expo/spawn-async';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import rimraf from 'rimraf';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
createFingerprintAsync,
|
|
8
|
-
createProjectHashAsync,
|
|
9
|
-
diffFingerprintChangesAsync,
|
|
10
|
-
} from '../../src/Fingerprint';
|
|
11
|
-
import { normalizeOptionsAsync } from '../../src/Options';
|
|
12
|
-
import { getHashSourcesAsync } from '../../src/sourcer/Sourcer';
|
|
13
|
-
|
|
14
|
-
jest.mock('../../src/sourcer/ExpoConfigLoader', () => ({
|
|
15
|
-
// Mock the getExpoConfigLoaderPath to use the built version rather than the typescript version from src
|
|
16
|
-
getExpoConfigLoaderPath: jest.fn(() =>
|
|
17
|
-
path.resolve(__dirname, '..', '..', 'build', 'sourcer', 'ExpoConfigLoader.js')
|
|
18
|
-
),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
describe('managed project test', () => {
|
|
22
|
-
jest.setTimeout(600000);
|
|
23
|
-
const tmpDir = require('temp-dir');
|
|
24
|
-
const projectName = 'fingerprint-e2e-managed';
|
|
25
|
-
const projectRoot = path.join(tmpDir, projectName);
|
|
26
|
-
|
|
27
|
-
beforeAll(async () => {
|
|
28
|
-
rimraf.sync(projectRoot);
|
|
29
|
-
await spawnAsync('bunx', ['create-expo-app', '-t', 'blank', projectName], {
|
|
30
|
-
stdio: 'inherit',
|
|
31
|
-
cwd: tmpDir,
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterAll(async () => {
|
|
36
|
-
rimraf.sync(projectRoot);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should have same hash after adding js only library', async () => {
|
|
40
|
-
const hash = await createProjectHashAsync(projectRoot);
|
|
41
|
-
await spawnAsync('npx', ['expo', 'install', '@react-navigation/core'], {
|
|
42
|
-
stdio: 'ignore',
|
|
43
|
-
cwd: projectRoot,
|
|
44
|
-
});
|
|
45
|
-
const hash2 = await createProjectHashAsync(projectRoot);
|
|
46
|
-
expect(hash).toBe(hash2);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should have same hash after updating js code', async () => {
|
|
50
|
-
const hash = await createProjectHashAsync(projectRoot);
|
|
51
|
-
|
|
52
|
-
const jsPath = path.join(projectRoot, 'App.js');
|
|
53
|
-
const js = await fs.readFile(jsPath, 'utf8');
|
|
54
|
-
await fs.writeFile(jsPath, `${js}\n// adding comments`);
|
|
55
|
-
|
|
56
|
-
const hash2 = await createProjectHashAsync(projectRoot);
|
|
57
|
-
expect(hash).toBe(hash2);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should have different hash after adding native library', async () => {
|
|
61
|
-
const hash = await createProjectHashAsync(projectRoot);
|
|
62
|
-
await spawnAsync('npx', ['expo', 'install', 'expo-updates'], {
|
|
63
|
-
stdio: 'ignore',
|
|
64
|
-
cwd: projectRoot,
|
|
65
|
-
});
|
|
66
|
-
const hash2 = await createProjectHashAsync(projectRoot);
|
|
67
|
-
expect(hash).not.toBe(hash2);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should have different hash after updating `jsEngine`', async () => {
|
|
71
|
-
const hash = await createProjectHashAsync(projectRoot);
|
|
72
|
-
|
|
73
|
-
const configPath = path.join(projectRoot, 'app.json');
|
|
74
|
-
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
75
|
-
config.expo.jsEngine = 'hermes';
|
|
76
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
77
|
-
|
|
78
|
-
const hash2 = await createProjectHashAsync(projectRoot);
|
|
79
|
-
expect(hash).not.toBe(hash2);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should have different hash after updating icon file', async () => {
|
|
83
|
-
const hash = await createProjectHashAsync(projectRoot);
|
|
84
|
-
|
|
85
|
-
const iconPath = path.join(projectRoot, 'assets', 'icon.png');
|
|
86
|
-
await fs.writeFile(iconPath, '');
|
|
87
|
-
|
|
88
|
-
const hash2 = await createProjectHashAsync(projectRoot);
|
|
89
|
-
expect(hash).not.toBe(hash2);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should have different hash after adding js only config-plugin', async () => {
|
|
93
|
-
const hash = await createProjectHashAsync(projectRoot);
|
|
94
|
-
await spawnAsync('npx', ['expo', 'install', 'expo-build-properties'], {
|
|
95
|
-
stdio: 'ignore',
|
|
96
|
-
cwd: projectRoot,
|
|
97
|
-
});
|
|
98
|
-
const hash2 = await createProjectHashAsync(projectRoot);
|
|
99
|
-
expect(hash).not.toBe(hash2);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('diffFingerprintChangesAsync - should return diff after adding native library', async () => {
|
|
103
|
-
const fingerprint = await createFingerprintAsync(projectRoot);
|
|
104
|
-
await spawnAsync('bun', ['install', '--save', '@react-native-community/netinfo@9.3.7'], {
|
|
105
|
-
stdio: 'ignore',
|
|
106
|
-
cwd: projectRoot,
|
|
107
|
-
});
|
|
108
|
-
const diff = await diffFingerprintChangesAsync(fingerprint, projectRoot);
|
|
109
|
-
expect(diff).toMatchInlineSnapshot(`
|
|
110
|
-
[
|
|
111
|
-
{
|
|
112
|
-
"filePath": "node_modules/@react-native-community/netinfo",
|
|
113
|
-
"hash": "8a255b59e10118a8cf5c1660d12d6b2e9293ed5c",
|
|
114
|
-
"reasons": [
|
|
115
|
-
"bareRncliAutolinking",
|
|
116
|
-
],
|
|
117
|
-
"type": "dir",
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
"contents": "{"@react-native-community/netinfo":{"root":"node_modules/@react-native-community/netinfo","name":"@react-native-community/netinfo","platforms":{"ios":{"podspecPath":"node_modules/@react-native-community/netinfo/react-native-netinfo.podspec","configurations":[],"scriptPhases":[]},"android":{"sourceDir":"node_modules/@react-native-community/netinfo/android","packageImportPath":"import com.reactnativecommunity.netinfo.NetInfoPackage;","packageInstance":"new NetInfoPackage()","buildTypes":[],"componentDescriptors":[],"cmakeListsPath":"node_modules/@react-native-community/netinfo/android/build/generated/source/codegen/jni/CMakeLists.txt"}}},"expo":{"root":"node_modules/expo","name":"expo","platforms":{"ios":{"podspecPath":"node_modules/expo/Expo.podspec","configurations":[],"scriptPhases":[]},"android":{"sourceDir":"node_modules/expo/android","packageImportPath":"import expo.modules.ExpoModulesPackage;","packageInstance":"new ExpoModulesPackage()","buildTypes":[],"componentDescriptors":[],"cmakeListsPath":"node_modules/expo/android/build/generated/source/codegen/jni/CMakeLists.txt"}}}}",
|
|
121
|
-
"hash": "ac75722bd87eb0189440be83faa2249079da5839",
|
|
122
|
-
"id": "rncliAutolinkingConfig",
|
|
123
|
-
"reasons": [
|
|
124
|
-
"bareRncliAutolinking",
|
|
125
|
-
],
|
|
126
|
-
"type": "contents",
|
|
127
|
-
},
|
|
128
|
-
]
|
|
129
|
-
`);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe(`getHashSourcesAsync - managed project`, () => {
|
|
134
|
-
jest.setTimeout(600000);
|
|
135
|
-
const tmpDir = require('temp-dir');
|
|
136
|
-
const projectName = 'fingerprint-e2e-managed';
|
|
137
|
-
const projectRoot = path.join(tmpDir, projectName);
|
|
138
|
-
|
|
139
|
-
beforeAll(async () => {
|
|
140
|
-
rimraf.sync(projectRoot);
|
|
141
|
-
// Pin the SDK version to prevent the latest version breaking snapshots
|
|
142
|
-
await spawnAsync('bunx', ['create-expo-app', '-t', 'blank@sdk-49', projectName], {
|
|
143
|
-
stdio: 'inherit',
|
|
144
|
-
cwd: tmpDir,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Pin the `expo` package version to prevent the latest version breaking snapshots
|
|
148
|
-
await spawnAsync('bun', ['install', '--save', 'expo@49.0.16'], {
|
|
149
|
-
stdio: 'ignore',
|
|
150
|
-
cwd: projectRoot,
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
afterAll(async () => {
|
|
155
|
-
rimraf.sync(projectRoot);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should match snapshot', async () => {
|
|
159
|
-
const sources = await getHashSourcesAsync(
|
|
160
|
-
projectRoot,
|
|
161
|
-
await normalizeOptionsAsync(projectRoot)
|
|
162
|
-
);
|
|
163
|
-
expect(sources).toMatchSnapshot();
|
|
164
|
-
});
|
|
165
|
-
});
|
package/e2e/jest.config.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
testEnvironment: 'node',
|
|
5
|
-
preset: 'ts-jest',
|
|
6
|
-
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
|
7
|
-
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
|
8
|
-
rootDir: path.resolve(__dirname),
|
|
9
|
-
displayName: require('../package').name,
|
|
10
|
-
roots: ['.'],
|
|
11
|
-
};
|
package/jest.config.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
testEnvironment: 'node',
|
|
5
|
-
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
|
6
|
-
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
|
7
|
-
rootDir: path.resolve(__dirname),
|
|
8
|
-
displayName: require('./package').name,
|
|
9
|
-
roots: ['__mocks__', 'src'],
|
|
10
|
-
};
|
package/scripts/createFixture.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool to generate vol JSON fixture from a project.
|
|
3
|
-
*
|
|
4
|
-
* Usage: npx ts-node scripts/createFixture /path/to/app /path/to/output.json
|
|
5
|
-
*/
|
|
6
|
-
import realFS from 'fs';
|
|
7
|
-
import glob from 'glob';
|
|
8
|
-
import { fs, vol } from 'memfs';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
|
|
11
|
-
function globAsync(pattern: string, options: Parameters<typeof glob>[1]): Promise<string[]> {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
glob(pattern, options, (err, matches) => {
|
|
14
|
-
if (err != null) {
|
|
15
|
-
reject(err);
|
|
16
|
-
} else {
|
|
17
|
-
resolve(matches);
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function createFixtureAsync(targetDir: string, outputFile: string) {
|
|
24
|
-
const files = await globAsync('**/*', {
|
|
25
|
-
cwd: targetDir,
|
|
26
|
-
ignore: [
|
|
27
|
-
// binary files
|
|
28
|
-
'**/*.{jpg,png}',
|
|
29
|
-
|
|
30
|
-
// lock files
|
|
31
|
-
'**/*.lock',
|
|
32
|
-
|
|
33
|
-
// node files
|
|
34
|
-
'**/node_modules/**',
|
|
35
|
-
|
|
36
|
-
// generated files
|
|
37
|
-
'**/build/**',
|
|
38
|
-
|
|
39
|
-
// ios files
|
|
40
|
-
'ios/Pods/**',
|
|
41
|
-
'vendor/**',
|
|
42
|
-
'**/xcuserdata/**',
|
|
43
|
-
'**/*.xcassets/**',
|
|
44
|
-
'**/IDEWorkspaceChecks.plist',
|
|
45
|
-
|
|
46
|
-
// android files
|
|
47
|
-
'**/*.jar',
|
|
48
|
-
'**/*.keystore',
|
|
49
|
-
'**/gradlew',
|
|
50
|
-
'**/gradlew.bat',
|
|
51
|
-
'**/gradle/wrapper/**',
|
|
52
|
-
],
|
|
53
|
-
nodir: true,
|
|
54
|
-
});
|
|
55
|
-
for (const file of files) {
|
|
56
|
-
const content = realFS.readFileSync(path.join(targetDir, file), 'utf8');
|
|
57
|
-
fs.mkdirSync(path.join('/', path.dirname(file)), { recursive: true });
|
|
58
|
-
fs.writeFileSync(path.join('/', file), content);
|
|
59
|
-
}
|
|
60
|
-
const resultJSON: Record<string, string | null> = {};
|
|
61
|
-
for (const [key, value] of Object.entries(vol.toJSON())) {
|
|
62
|
-
resultJSON[path.join('/app', path.relative('/', key))] = value;
|
|
63
|
-
}
|
|
64
|
-
realFS.writeFileSync(outputFile, JSON.stringify(resultJSON, null, 2));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
(async () => {
|
|
68
|
-
if (process.argv.length !== 4) {
|
|
69
|
-
console.log(`Usage: ${path.basename(process.argv[1])} targetDir outputFile`);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
const targetDir = process.argv[2];
|
|
73
|
-
const outputFile = process.argv[3];
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
await createFixtureAsync(targetDir, outputFile);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
console.error('Uncaught Error', e);
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
})();
|
package/src/Dedup.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import assert from 'assert';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
import type { HashSource, HashSourceDir, HashSourceFile } from './Fingerprint.types';
|
|
5
|
-
|
|
6
|
-
const debug = require('debug')('expo:fingerprint:Dedup');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Strip duplicated sources, mainly for duplicated file or dir
|
|
10
|
-
*/
|
|
11
|
-
export function dedupSources(sources: HashSource[], projectRoot: string): HashSource[] {
|
|
12
|
-
const newSources: HashSource[] = [];
|
|
13
|
-
for (const source of sources) {
|
|
14
|
-
const [duplicatedItemIndex, shouldSwapSource] = findDuplicatedSourceIndex(
|
|
15
|
-
newSources,
|
|
16
|
-
source,
|
|
17
|
-
projectRoot
|
|
18
|
-
);
|
|
19
|
-
if (duplicatedItemIndex >= 0) {
|
|
20
|
-
const duplicatedItem = newSources[duplicatedItemIndex];
|
|
21
|
-
debug(`Skipping duplicated source: ${source}`);
|
|
22
|
-
if (shouldSwapSource) {
|
|
23
|
-
newSources[duplicatedItemIndex] = {
|
|
24
|
-
...source,
|
|
25
|
-
reasons: [...source.reasons, ...duplicatedItem.reasons],
|
|
26
|
-
};
|
|
27
|
-
} else {
|
|
28
|
-
duplicatedItem.reasons.push(...source.reasons);
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
newSources.push(source);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return newSources;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* When two sources are duplicated, merge `src`'s reasons into `dst`
|
|
40
|
-
*/
|
|
41
|
-
export function mergeSourceWithReasons(dst: HashSource, src: HashSource): HashSource {
|
|
42
|
-
return dst;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Find the duplicated `source` in `newSources`
|
|
47
|
-
* @return tuple of [duplicatedItemIndexInNewSources, shouldSwapSource]
|
|
48
|
-
*/
|
|
49
|
-
function findDuplicatedSourceIndex(
|
|
50
|
-
newSources: HashSource[],
|
|
51
|
-
source: HashSource,
|
|
52
|
-
projectRoot: string
|
|
53
|
-
): [number, boolean] {
|
|
54
|
-
let shouldSwapSource = false;
|
|
55
|
-
if (source.type === 'contents') {
|
|
56
|
-
return [
|
|
57
|
-
newSources.findIndex((item) => item.type === source.type && item.id === source.id) ?? null,
|
|
58
|
-
shouldSwapSource,
|
|
59
|
-
];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
for (const [index, existingSource] of newSources.entries()) {
|
|
63
|
-
if (existingSource.type === 'contents') {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (isDescendant(source, existingSource, projectRoot)) {
|
|
67
|
-
return [index, shouldSwapSource];
|
|
68
|
-
}
|
|
69
|
-
// If the new source is ancestor of existing source, replace swap the existing source with the new source
|
|
70
|
-
if (isDescendant(existingSource, source, projectRoot)) {
|
|
71
|
-
shouldSwapSource = true;
|
|
72
|
-
return [index, shouldSwapSource];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return [-1, shouldSwapSource];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isDescendant(
|
|
79
|
-
from: HashSourceDir | HashSourceFile,
|
|
80
|
-
to: HashSourceDir | HashSourceFile,
|
|
81
|
-
projectRoot: string
|
|
82
|
-
): boolean {
|
|
83
|
-
if (from === to) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const fromPath = path.join(projectRoot, from.filePath);
|
|
88
|
-
const toPath = path.join(projectRoot, to.filePath);
|
|
89
|
-
const result = path.relative(fromPath, toPath).match(/^[./\\/]*$/) != null;
|
|
90
|
-
if (result) {
|
|
91
|
-
assert(
|
|
92
|
-
!(to.type === 'file' && from.type === 'dir'),
|
|
93
|
-
`Unexpected case which a dir is a descendant of a file - from[${fromPath}] to[${toPath}]`
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
}
|
package/src/Fingerprint.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { dedupSources } from './Dedup';
|
|
2
|
-
import type { Fingerprint, FingerprintSource, Options } from './Fingerprint.types';
|
|
3
|
-
import { normalizeOptionsAsync } from './Options';
|
|
4
|
-
import { sortSources } from './Sort';
|
|
5
|
-
import { createFingerprintFromSourcesAsync } from './hash/Hash';
|
|
6
|
-
import { getHashSourcesAsync } from './sourcer/Sourcer';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Create a fingerprint from project
|
|
10
|
-
*/
|
|
11
|
-
export async function createFingerprintAsync(
|
|
12
|
-
projectRoot: string,
|
|
13
|
-
options?: Options
|
|
14
|
-
): Promise<Fingerprint> {
|
|
15
|
-
const opts = await normalizeOptionsAsync(projectRoot, options);
|
|
16
|
-
const sources = await getHashSourcesAsync(projectRoot, opts);
|
|
17
|
-
const normalizedSources = sortSources(dedupSources(sources, projectRoot));
|
|
18
|
-
const fingerprint = await createFingerprintFromSourcesAsync(normalizedSources, projectRoot, opts);
|
|
19
|
-
return fingerprint;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Create a native hash value from project
|
|
24
|
-
*/
|
|
25
|
-
export async function createProjectHashAsync(
|
|
26
|
-
projectRoot: string,
|
|
27
|
-
options?: Options
|
|
28
|
-
): Promise<string> {
|
|
29
|
-
const fingerprint = await createFingerprintAsync(projectRoot, options);
|
|
30
|
-
return fingerprint.hash;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Differentiate given `fingerprint` with the current project fingerprint state
|
|
35
|
-
*/
|
|
36
|
-
export async function diffFingerprintChangesAsync(
|
|
37
|
-
fingerprint: Fingerprint,
|
|
38
|
-
projectRoot: string,
|
|
39
|
-
options?: Options
|
|
40
|
-
): Promise<FingerprintSource[]> {
|
|
41
|
-
const newFingerprint = await createFingerprintAsync(projectRoot, options);
|
|
42
|
-
if (fingerprint.hash === newFingerprint.hash) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
return diffFingerprints(fingerprint, newFingerprint);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Differentiate two fingerprints
|
|
50
|
-
*/
|
|
51
|
-
export function diffFingerprints(
|
|
52
|
-
fingerprint1: Fingerprint,
|
|
53
|
-
fingerprint2: Fingerprint
|
|
54
|
-
): FingerprintSource[] {
|
|
55
|
-
return fingerprint2.sources.filter((newItem) => {
|
|
56
|
-
return !fingerprint1.sources.find(
|
|
57
|
-
(item) => item.type === newItem.type && item.hash === newItem.hash
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
}
|
package/src/Fingerprint.types.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
export type FingerprintSource = HashSource & {
|
|
2
|
-
/**
|
|
3
|
-
* Hash value of the `source`.
|
|
4
|
-
* If the source is excluding by `Options.dirExcludes`, the value will be null.
|
|
5
|
-
*/
|
|
6
|
-
hash: string | null;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export interface Fingerprint {
|
|
10
|
-
/**
|
|
11
|
-
* Sources and their hash values to generate a fingerprint
|
|
12
|
-
*/
|
|
13
|
-
sources: FingerprintSource[];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The final hash value of the whole fingerprint
|
|
17
|
-
*/
|
|
18
|
-
hash: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type Platform = 'android' | 'ios';
|
|
22
|
-
|
|
23
|
-
export interface Options {
|
|
24
|
-
/**
|
|
25
|
-
* Only get native files from the given platforms. Default is `['android', 'ios']`.
|
|
26
|
-
*/
|
|
27
|
-
platforms?: Platform[];
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* I/O concurrent limit. Default is the number of CPU core.
|
|
31
|
-
*/
|
|
32
|
-
concurrentIoLimit?: number;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The algorithm passing to `crypto.createHash()`. Default is `'sha1'`.
|
|
36
|
-
*/
|
|
37
|
-
hashAlgorithm?: string;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Excludes directories from hashing. This supported pattern is as `glob()`.
|
|
41
|
-
* Default is `['android/build', 'android/app/build', 'android/app/.cxx', 'ios/Pods']`.
|
|
42
|
-
* @deprecated Use `ignorePaths` instead.
|
|
43
|
-
*/
|
|
44
|
-
dirExcludes?: string[];
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Ignore files and directories from hashing. This supported pattern is as `glob()`.
|
|
48
|
-
*
|
|
49
|
-
* Please note that the pattern matching is slightly different from gitignore. For example, we don't support partial matching where `build` does not match `android/build`. You should use `'**' + '/build'` instead.
|
|
50
|
-
* @see [minimatch implementations](https://github.com/isaacs/minimatch#comparisons-to-other-fnmatchglob-implementations) for more reference.
|
|
51
|
-
*
|
|
52
|
-
* Besides this `ignorePaths`, fingerprint comes with implicit default ignorePaths defined in `Options.DEFAULT_IGNORE_PATHS`.
|
|
53
|
-
* If you want to override the default ignorePaths, use `!` prefix.
|
|
54
|
-
*/
|
|
55
|
-
ignorePaths?: string[];
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Additional sources for hashing.
|
|
59
|
-
*/
|
|
60
|
-
extraSources?: HashSource[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
//#region internal types
|
|
64
|
-
|
|
65
|
-
export interface NormalizedOptions extends Options {
|
|
66
|
-
platforms: NonNullable<Options['platforms']>;
|
|
67
|
-
concurrentIoLimit: NonNullable<Options['concurrentIoLimit']>;
|
|
68
|
-
hashAlgorithm: NonNullable<Options['hashAlgorithm']>;
|
|
69
|
-
ignorePaths: NonNullable<Options['ignorePaths']>;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface HashSourceFile {
|
|
73
|
-
type: 'file';
|
|
74
|
-
filePath: string;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Reasons of this source coming from
|
|
78
|
-
*/
|
|
79
|
-
reasons: string[];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface HashSourceDir {
|
|
83
|
-
type: 'dir';
|
|
84
|
-
filePath: string;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Reasons of this source coming from
|
|
88
|
-
*/
|
|
89
|
-
reasons: string[];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface HashSourceContents {
|
|
93
|
-
type: 'contents';
|
|
94
|
-
id: string;
|
|
95
|
-
contents: string | Buffer;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Reasons of this source coming from
|
|
99
|
-
*/
|
|
100
|
-
reasons: string[];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export type HashSource = HashSourceFile | HashSourceDir | HashSourceContents;
|
|
104
|
-
|
|
105
|
-
export interface HashResult {
|
|
106
|
-
id: string;
|
|
107
|
-
hex: string;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
//#endregion
|
package/src/Options.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
import type { NormalizedOptions, Options } from './Fingerprint.types';
|
|
6
|
-
|
|
7
|
-
export const FINGERPRINT_IGNORE_FILENAME = '.fingerprintignore';
|
|
8
|
-
|
|
9
|
-
export const DEFAULT_IGNORE_PATHS = [
|
|
10
|
-
FINGERPRINT_IGNORE_FILENAME,
|
|
11
|
-
'**/android/build/**/*',
|
|
12
|
-
'**/android/app/build/**/*',
|
|
13
|
-
'**/android/app/.cxx/**/*',
|
|
14
|
-
'**/ios/Pods/**/*',
|
|
15
|
-
|
|
16
|
-
// Ignore all expo configs because we will read expo config in a HashSourceContents already
|
|
17
|
-
'app.config.ts',
|
|
18
|
-
'app.config.js',
|
|
19
|
-
'app.config.json',
|
|
20
|
-
'app.json',
|
|
21
|
-
|
|
22
|
-
// Ignore default javascript files when calling `getConfig()`
|
|
23
|
-
'**/node_modules/@babel/**/*',
|
|
24
|
-
'**/node_modules/@expo/**/*',
|
|
25
|
-
'**/node_modules/@jridgewell/**/*',
|
|
26
|
-
'**/node_modules/expo/config.js',
|
|
27
|
-
'**/node_modules/expo/config-plugins.js',
|
|
28
|
-
`**/node_modules/{${[
|
|
29
|
-
'debug',
|
|
30
|
-
'escape-string-regexp',
|
|
31
|
-
'getenv',
|
|
32
|
-
'graceful-fs',
|
|
33
|
-
'has-flag',
|
|
34
|
-
'imurmurhash',
|
|
35
|
-
'js-tokens',
|
|
36
|
-
'json5',
|
|
37
|
-
'lines-and-columns',
|
|
38
|
-
'require-from-string',
|
|
39
|
-
'resolve-from',
|
|
40
|
-
'signal-exit',
|
|
41
|
-
'sucrase',
|
|
42
|
-
'supports-color',
|
|
43
|
-
'ts-interface-checker',
|
|
44
|
-
'write-file-atomic',
|
|
45
|
-
].join(',')}}/**/*`,
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
export async function normalizeOptionsAsync(
|
|
49
|
-
projectRoot: string,
|
|
50
|
-
options?: Options
|
|
51
|
-
): Promise<NormalizedOptions> {
|
|
52
|
-
return {
|
|
53
|
-
...options,
|
|
54
|
-
platforms: options?.platforms ?? ['android', 'ios'],
|
|
55
|
-
concurrentIoLimit: options?.concurrentIoLimit ?? os.cpus().length,
|
|
56
|
-
hashAlgorithm: options?.hashAlgorithm ?? 'sha1',
|
|
57
|
-
ignorePaths: await collectIgnorePathsAsync(projectRoot, options),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function collectIgnorePathsAsync(projectRoot: string, options?: Options): Promise<string[]> {
|
|
62
|
-
const ignorePaths = [
|
|
63
|
-
...DEFAULT_IGNORE_PATHS,
|
|
64
|
-
...(options?.ignorePaths ?? []),
|
|
65
|
-
...(options?.dirExcludes?.map((dirExclude) => `${dirExclude}/**/*`) ?? []),
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
const fingerprintIgnorePath = path.join(projectRoot, FINGERPRINT_IGNORE_FILENAME);
|
|
69
|
-
try {
|
|
70
|
-
const fingerprintIgnore = await fs.readFile(fingerprintIgnorePath, 'utf8');
|
|
71
|
-
const fingerprintIgnoreLines = fingerprintIgnore.split('\n');
|
|
72
|
-
for (const line of fingerprintIgnoreLines) {
|
|
73
|
-
const trimmedLine = line.trim();
|
|
74
|
-
if (trimmedLine) {
|
|
75
|
-
ignorePaths.push(trimmedLine);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
} catch {}
|
|
79
|
-
|
|
80
|
-
return ignorePaths;
|
|
81
|
-
}
|
package/src/Sort.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { HashSource } from './Fingerprint.types';
|
|
2
|
-
|
|
3
|
-
export function sortSources<T extends HashSource>(sources: T[]): T[] {
|
|
4
|
-
const typeOrder = {
|
|
5
|
-
file: 0,
|
|
6
|
-
dir: 1,
|
|
7
|
-
contents: 2,
|
|
8
|
-
};
|
|
9
|
-
return sources.sort((a, b) => {
|
|
10
|
-
const typeResult = typeOrder[a.type] - typeOrder[b.type];
|
|
11
|
-
if (typeResult === 0) {
|
|
12
|
-
if (a.type === 'file' && b.type === 'file') {
|
|
13
|
-
return a.filePath.localeCompare(b.filePath);
|
|
14
|
-
} else if (a.type === 'dir' && b.type === 'dir') {
|
|
15
|
-
return a.filePath.localeCompare(b.filePath);
|
|
16
|
-
} else if (a.type === 'contents' && b.type === 'contents') {
|
|
17
|
-
return a.id.localeCompare(b.id);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return typeResult;
|
|
21
|
-
});
|
|
22
|
-
}
|