@expo/fingerprint 0.0.1
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/.eslintignore +1 -0
- package/LICENSE +22 -0
- package/README.md +118 -0
- package/__mocks__/fs/promises.ts +2 -0
- package/__mocks__/fs.ts +2 -0
- package/babel.config.js +6 -0
- package/bin/cli.js +27 -0
- package/build/Dedup.d.ts +9 -0
- package/build/Dedup.js +83 -0
- package/build/Dedup.js.map +1 -0
- package/build/Fingerprint.d.ts +13 -0
- package/build/Fingerprint.js +42 -0
- package/build/Fingerprint.js.map +1 -0
- package/build/Fingerprint.types.d.ts +78 -0
- package/build/Fingerprint.types.js +4 -0
- package/build/Fingerprint.types.js.map +1 -0
- package/build/Options.d.ts +2 -0
- package/build/Options.js +23 -0
- package/build/Options.js.map +1 -0
- package/build/Sort.d.ts +2 -0
- package/build/Sort.js +27 -0
- package/build/Sort.js.map +1 -0
- package/build/hash/Hash.d.ts +28 -0
- package/build/hash/Hash.js +166 -0
- package/build/hash/Hash.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +22 -0
- package/build/index.js.map +1 -0
- package/build/sourcer/Bare.d.ts +6 -0
- package/build/sourcer/Bare.js +107 -0
- package/build/sourcer/Bare.js.map +1 -0
- package/build/sourcer/Expo.d.ts +5 -0
- package/build/sourcer/Expo.js +175 -0
- package/build/sourcer/Expo.js.map +1 -0
- package/build/sourcer/PatchPackage.d.ts +2 -0
- package/build/sourcer/PatchPackage.js +19 -0
- package/build/sourcer/PatchPackage.js.map +1 -0
- package/build/sourcer/Sourcer.d.ts +2 -0
- package/build/sourcer/Sourcer.js +42 -0
- package/build/sourcer/Sourcer.js.map +1 -0
- package/build/sourcer/Utils.d.ts +2 -0
- package/build/sourcer/Utils.js +25 -0
- package/build/sourcer/Utils.js.map +1 -0
- package/build/utils/Profile.d.ts +8 -0
- package/build/utils/Profile.js +43 -0
- package/build/utils/Profile.js.map +1 -0
- package/e2e/__tests__/__snapshots__/managed-test.ts.snap +200 -0
- package/e2e/__tests__/bare-test.ts +66 -0
- package/e2e/__tests__/managed-test.ts +162 -0
- package/e2e/jest.config.js +10 -0
- package/jest.config.js +10 -0
- package/package.json +70 -0
- package/scripts/createFixture.ts +81 -0
- package/src/Dedup.ts +97 -0
- package/src/Fingerprint.ts +51 -0
- package/src/Fingerprint.types.ts +98 -0
- package/src/Options.ts +18 -0
- package/src/Sort.ts +22 -0
- package/src/__tests__/Dedup-test.ts +177 -0
- package/src/__tests__/Fingerprint-test.ts +116 -0
- package/src/__tests__/Sort-test.ts +56 -0
- package/src/hash/Hash.ts +204 -0
- package/src/hash/__tests__/Hash-test.ts +192 -0
- package/src/index.ts +3 -0
- package/src/sourcer/Bare.ts +114 -0
- package/src/sourcer/Expo.ts +216 -0
- package/src/sourcer/PatchPackage.ts +18 -0
- package/src/sourcer/Sourcer.ts +58 -0
- package/src/sourcer/Utils.ts +23 -0
- package/src/sourcer/__tests__/Bare-test.ts +59 -0
- package/src/sourcer/__tests__/Expo-test.ts +265 -0
- package/src/sourcer/__tests__/PatchPackage-test.ts +54 -0
- package/src/sourcer/__tests__/Sourcer-test.ts +14 -0
- package/src/sourcer/__tests__/__snapshots__/Bare-test.ts.snap +29 -0
- package/src/sourcer/__tests__/__snapshots__/Expo-test.ts.snap +139 -0
- package/src/sourcer/__tests__/fixtures/BareReactNative70Project.json +47 -0
- package/src/sourcer/__tests__/fixtures/ExpoAutolinkingAndroid.json +82 -0
- package/src/sourcer/__tests__/fixtures/ExpoAutolinkingIos.json +114 -0
- package/src/sourcer/__tests__/fixtures/ExpoManaged47Project.json +6 -0
- package/src/sourcer/__tests__/fixtures/PatchPackage.json +4 -0
- package/src/sourcer/__tests__/fixtures/RncliAutoLinking.json +165 -0
- package/src/utils/Profile.ts +47 -0
- package/src/utils/__tests__/Profile-test.ts +11 -0
- package/tsconfig.json +9 -0
package/src/Dedup.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { dedupSources } from './Dedup';
|
|
2
|
+
import type { Fingerprint, FingerprintSource, Options } from './Fingerprint.types';
|
|
3
|
+
import { normalizeOptions } 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 = normalizeOptions(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
|
+
const result: FingerprintSource[] = newFingerprint.sources.filter((newItem) => {
|
|
46
|
+
return !fingerprint.sources.find(
|
|
47
|
+
(item) => item.type === newItem.type && item.hash === newItem.hash
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
*/
|
|
43
|
+
dirExcludes?: string[];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Additional sources for hashing.
|
|
47
|
+
*/
|
|
48
|
+
extraSources?: HashSource[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#region internal types
|
|
52
|
+
|
|
53
|
+
export interface NormalizedOptions extends Options {
|
|
54
|
+
platforms: NonNullable<Options['platforms']>;
|
|
55
|
+
concurrentIoLimit: NonNullable<Options['concurrentIoLimit']>;
|
|
56
|
+
hashAlgorithm: NonNullable<Options['hashAlgorithm']>;
|
|
57
|
+
dirExcludes: NonNullable<Options['dirExcludes']>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface HashSourceFile {
|
|
61
|
+
type: 'file';
|
|
62
|
+
filePath: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reasons of this source coming from
|
|
66
|
+
*/
|
|
67
|
+
reasons: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface HashSourceDir {
|
|
71
|
+
type: 'dir';
|
|
72
|
+
filePath: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reasons of this source coming from
|
|
76
|
+
*/
|
|
77
|
+
reasons: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface HashSourceContents {
|
|
81
|
+
type: 'contents';
|
|
82
|
+
id: string;
|
|
83
|
+
contents: string | Buffer;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Reasons of this source coming from
|
|
87
|
+
*/
|
|
88
|
+
reasons: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type HashSource = HashSourceFile | HashSourceDir | HashSourceContents;
|
|
92
|
+
|
|
93
|
+
export interface HashResult {
|
|
94
|
+
id: string;
|
|
95
|
+
hex: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//#endregion
|
package/src/Options.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
|
|
3
|
+
import type { NormalizedOptions, Options } from './Fingerprint.types';
|
|
4
|
+
|
|
5
|
+
export function normalizeOptions(options?: Options): NormalizedOptions {
|
|
6
|
+
return {
|
|
7
|
+
...options,
|
|
8
|
+
platforms: options?.platforms ?? ['android', 'ios'],
|
|
9
|
+
concurrentIoLimit: options?.concurrentIoLimit ?? os.cpus().length,
|
|
10
|
+
hashAlgorithm: options?.hashAlgorithm ?? 'sha1',
|
|
11
|
+
dirExcludes: options?.dirExcludes ?? [
|
|
12
|
+
'**/android/build',
|
|
13
|
+
'**/android/app/build',
|
|
14
|
+
'**/android/app/.cxx',
|
|
15
|
+
'ios/Pods',
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
}
|
package/src/Sort.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { dedupSources } from '../Dedup';
|
|
2
|
+
import type { HashSource } from '../Fingerprint.types';
|
|
3
|
+
|
|
4
|
+
describe(dedupSources, () => {
|
|
5
|
+
it('should dedup descendant dir - ancestor coming first', () => {
|
|
6
|
+
const sources: HashSource[] = [
|
|
7
|
+
{
|
|
8
|
+
type: 'dir',
|
|
9
|
+
filePath: 'android',
|
|
10
|
+
reasons: ['root'],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: 'dir',
|
|
14
|
+
filePath: 'android/app',
|
|
15
|
+
reasons: ['subfolder'],
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
expect(dedupSources(sources, '/app')).toEqual([
|
|
20
|
+
{
|
|
21
|
+
type: 'dir',
|
|
22
|
+
filePath: 'android',
|
|
23
|
+
reasons: ['root', 'subfolder'],
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should dedup descendant dir - ancestor coming later', () => {
|
|
29
|
+
const sources: HashSource[] = [
|
|
30
|
+
{
|
|
31
|
+
type: 'dir',
|
|
32
|
+
filePath: 'android/app',
|
|
33
|
+
reasons: ['subfolder'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'dir',
|
|
37
|
+
filePath: 'android',
|
|
38
|
+
reasons: ['root'],
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
expect(dedupSources(sources, '/app')).toEqual([
|
|
43
|
+
{
|
|
44
|
+
type: 'dir',
|
|
45
|
+
filePath: 'android',
|
|
46
|
+
reasons: ['root', 'subfolder'],
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should dedup descendant file - file coming first', () => {
|
|
52
|
+
const sources: HashSource[] = [
|
|
53
|
+
{
|
|
54
|
+
type: 'file',
|
|
55
|
+
filePath: 'android/app/build.gradle',
|
|
56
|
+
reasons: ['file'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'dir',
|
|
60
|
+
filePath: 'android',
|
|
61
|
+
reasons: ['root'],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
expect(dedupSources(sources, '/app')).toEqual([
|
|
66
|
+
{
|
|
67
|
+
type: 'dir',
|
|
68
|
+
filePath: 'android',
|
|
69
|
+
reasons: ['root', 'file'],
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should dedup descendant file - file coming later', () => {
|
|
75
|
+
const sources: HashSource[] = [
|
|
76
|
+
{
|
|
77
|
+
type: 'dir',
|
|
78
|
+
filePath: 'android',
|
|
79
|
+
reasons: ['root'],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'file',
|
|
83
|
+
filePath: 'android/app/build.gradle',
|
|
84
|
+
reasons: ['file'],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
expect(dedupSources(sources, '/app')).toEqual([
|
|
89
|
+
{
|
|
90
|
+
type: 'dir',
|
|
91
|
+
filePath: 'android',
|
|
92
|
+
reasons: ['root', 'file'],
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should dedup slibings dir', () => {
|
|
98
|
+
const sources: HashSource[] = [
|
|
99
|
+
{
|
|
100
|
+
type: 'dir',
|
|
101
|
+
filePath: 'node_modules/expo',
|
|
102
|
+
reasons: ['expo'],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'dir',
|
|
106
|
+
filePath: 'node_modules/expo-modules-core',
|
|
107
|
+
reasons: ['expo-modules-core'],
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
expect(dedupSources(sources, '/app')).toEqual([
|
|
112
|
+
{
|
|
113
|
+
type: 'dir',
|
|
114
|
+
filePath: 'node_modules/expo',
|
|
115
|
+
reasons: ['expo'],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'dir',
|
|
119
|
+
filePath: 'node_modules/expo-modules-core',
|
|
120
|
+
reasons: ['expo-modules-core'],
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should keep unique contents by its id', () => {
|
|
126
|
+
const sources: HashSource[] = [
|
|
127
|
+
{
|
|
128
|
+
type: 'contents',
|
|
129
|
+
id: 'foo',
|
|
130
|
+
contents: 'foo:1',
|
|
131
|
+
reasons: ['foo:1'],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'dir',
|
|
135
|
+
filePath: 'android',
|
|
136
|
+
reasons: ['root'],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'contents',
|
|
140
|
+
id: 'foo',
|
|
141
|
+
contents: 'foo:2',
|
|
142
|
+
reasons: ['foo:2'],
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
expect(dedupSources(sources, '/app')).toEqual([
|
|
147
|
+
{
|
|
148
|
+
type: 'contents',
|
|
149
|
+
id: 'foo',
|
|
150
|
+
contents: 'foo:1',
|
|
151
|
+
reasons: ['foo:1', 'foo:2'],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
type: 'dir',
|
|
155
|
+
filePath: 'android',
|
|
156
|
+
reasons: ['root'],
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should throw error when a dir is descendant of a file', () => {
|
|
162
|
+
const sources: HashSource[] = [
|
|
163
|
+
{
|
|
164
|
+
type: 'file',
|
|
165
|
+
filePath: 'android',
|
|
166
|
+
reasons: ['file'],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: 'dir',
|
|
170
|
+
filePath: 'android/app',
|
|
171
|
+
reasons: ['dir'],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
expect(() => dedupSources(sources, '/app')).toThrow();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { vol } from 'memfs';
|
|
2
|
+
|
|
3
|
+
import { createFingerprintAsync, diffFingerprintChangesAsync } from '../Fingerprint';
|
|
4
|
+
import type { Fingerprint } from '../Fingerprint.types';
|
|
5
|
+
import { normalizeOptions } from '../Options';
|
|
6
|
+
|
|
7
|
+
jest.mock('fs');
|
|
8
|
+
jest.mock('fs/promises');
|
|
9
|
+
|
|
10
|
+
describe(diffFingerprintChangesAsync, () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vol.reset();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return empty array when fingerprint matched', async () => {
|
|
16
|
+
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
|
|
17
|
+
const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
|
|
18
|
+
const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
|
|
19
|
+
expect(diff.length).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return diff from new item', async () => {
|
|
23
|
+
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
|
|
24
|
+
const fingerprint: Fingerprint = {
|
|
25
|
+
sources: [],
|
|
26
|
+
hash: '',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
|
|
30
|
+
expect(diff).toMatchInlineSnapshot(`
|
|
31
|
+
Array [
|
|
32
|
+
Object {
|
|
33
|
+
"filePath": "app.json",
|
|
34
|
+
"hash": "1fd2d92d50dc1da96b41795046b9ea4e30dd2b48",
|
|
35
|
+
"reasons": Array [
|
|
36
|
+
"expoConfig",
|
|
37
|
+
],
|
|
38
|
+
"type": "file",
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return diff from contents changes', async () => {
|
|
45
|
+
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
|
|
46
|
+
const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8').toString());
|
|
47
|
+
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
|
|
48
|
+
const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
|
|
49
|
+
|
|
50
|
+
// first round for bumping package version which should not cause changes
|
|
51
|
+
packageJson.version = '111.111.111';
|
|
52
|
+
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
|
|
53
|
+
let diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
|
|
54
|
+
expect(diff.length).toBe(0);
|
|
55
|
+
|
|
56
|
+
// second round to update scripts section and it should cause changes
|
|
57
|
+
packageJson.scripts ||= {};
|
|
58
|
+
packageJson.scripts.postinstall = 'echo "hello"';
|
|
59
|
+
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
|
|
60
|
+
diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
|
|
61
|
+
jest.dontMock('/app/package.json');
|
|
62
|
+
expect(diff).toMatchInlineSnapshot(`
|
|
63
|
+
Array [
|
|
64
|
+
Object {
|
|
65
|
+
"contents": "{\\"start\\":\\"expo start\\",\\"android\\":\\"expo start --android\\",\\"ios\\":\\"expo start --ios\\",\\"web\\":\\"expo start --web\\",\\"postinstall\\":\\"echo \\\\\\"hello\\\\\\"\\"}",
|
|
66
|
+
"hash": "47f4d7bae018eb17440153f09977113501eace30",
|
|
67
|
+
"id": "packageJson:scripts",
|
|
68
|
+
"reasons": Array [
|
|
69
|
+
"packageJson:scripts",
|
|
70
|
+
],
|
|
71
|
+
"type": "contents",
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return diff from file changes', async () => {
|
|
78
|
+
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
|
|
79
|
+
const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
|
|
80
|
+
const config = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
|
|
81
|
+
config.expo.jsEngine = 'jsc';
|
|
82
|
+
vol.writeFileSync('/app/app.json', JSON.stringify(config, null, 2));
|
|
83
|
+
const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
|
|
84
|
+
expect(diff).toMatchInlineSnapshot(`
|
|
85
|
+
Array [
|
|
86
|
+
Object {
|
|
87
|
+
"filePath": "app.json",
|
|
88
|
+
"hash": "9ff1b51ca9b9435e8b849bcc82e3900d70f0feee",
|
|
89
|
+
"reasons": Array [
|
|
90
|
+
"expoConfig",
|
|
91
|
+
],
|
|
92
|
+
"type": "file",
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return diff from dir changes', async () => {
|
|
99
|
+
vol.fromJSON(require('../sourcer/__tests__/fixtures/BareReactNative70Project.json'));
|
|
100
|
+
const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
|
|
101
|
+
vol.writeFileSync('/app/ios/README.md', '# Adding new file in ios dir');
|
|
102
|
+
const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
|
|
103
|
+
expect(diff).toMatchInlineSnapshot(`
|
|
104
|
+
Array [
|
|
105
|
+
Object {
|
|
106
|
+
"filePath": "ios",
|
|
107
|
+
"hash": "e4190c0af9142fe4add4842777d9aec713213cd4",
|
|
108
|
+
"reasons": Array [
|
|
109
|
+
"bareNativeDir",
|
|
110
|
+
],
|
|
111
|
+
"type": "dir",
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
`);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { FingerprintSource, HashSource } from '../Fingerprint.types';
|
|
2
|
+
import { sortSources } from '../Sort';
|
|
3
|
+
|
|
4
|
+
describe(sortSources, () => {
|
|
5
|
+
it(`should sort sources by type in 'file > dir > contents' order`, () => {
|
|
6
|
+
const sources: HashSource[] = [
|
|
7
|
+
{ type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
|
|
8
|
+
{ type: 'file', filePath: '/app/app.json', reasons: ['expoConfig'] },
|
|
9
|
+
{ type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
expect(sortSources(sources)).toEqual([
|
|
13
|
+
{ type: 'file', filePath: '/app/app.json', reasons: ['expoConfig'] },
|
|
14
|
+
{ type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] },
|
|
15
|
+
{ type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it(`should sort id or filePath when item types are the same`, () => {
|
|
20
|
+
const sources: HashSource[] = [
|
|
21
|
+
{ type: 'file', filePath: '/app/eas.json', reasons: ['easBuild'] },
|
|
22
|
+
{ type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
|
|
23
|
+
{ type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] },
|
|
24
|
+
{ type: 'file', filePath: '/app/app.json', reasons: ['expoConfig'] },
|
|
25
|
+
{ type: 'contents', id: 'bar', contents: 'bartender', reasons: ['bar'] },
|
|
26
|
+
{ type: 'dir', filePath: '/app/android', reasons: ['bareNativeDir'] },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
expect(sortSources(sources)).toEqual([
|
|
30
|
+
{ type: 'file', filePath: '/app/app.json', reasons: ['expoConfig'] },
|
|
31
|
+
{ type: 'file', filePath: '/app/eas.json', reasons: ['easBuild'] },
|
|
32
|
+
{ type: 'dir', filePath: '/app/android', reasons: ['bareNativeDir'] },
|
|
33
|
+
{ type: 'dir', filePath: '/app/ios', reasons: ['bareNativeDir'] },
|
|
34
|
+
{ type: 'contents', id: 'bar', contents: 'bartender', reasons: ['bar'] },
|
|
35
|
+
{ type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it(`should support both HashSource and FingerprintSource types`, () => {
|
|
40
|
+
const sources: HashSource[] = [
|
|
41
|
+
{ type: 'contents', id: 'foo', contents: 'HelloWorld', reasons: ['foo'] },
|
|
42
|
+
];
|
|
43
|
+
const fingerprintSources: FingerprintSource[] = [
|
|
44
|
+
{
|
|
45
|
+
type: 'contents',
|
|
46
|
+
id: 'foo',
|
|
47
|
+
contents: 'HelloWorld',
|
|
48
|
+
reasons: ['foo'],
|
|
49
|
+
hash: 'bc9faaae1e35d52f3dea9651da12cd36627b8403',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
expect(sortSources(sources)).toEqual(sources);
|
|
54
|
+
expect(sortSources(fingerprintSources)).toEqual(fingerprintSources);
|
|
55
|
+
});
|
|
56
|
+
});
|