@expo/fingerprint 0.1.0 → 0.3.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +70 -0
  3. package/__mocks__/@expo/spawn-async.ts +30 -0
  4. package/__mocks__/fs/promises.ts +15 -0
  5. package/build/Fingerprint.js +1 -1
  6. package/build/Fingerprint.js.map +1 -1
  7. package/build/Fingerprint.types.d.ts +12 -1
  8. package/build/Fingerprint.types.js.map +1 -1
  9. package/build/Options.d.ts +3 -1
  10. package/build/Options.js +63 -9
  11. package/build/Options.js.map +1 -1
  12. package/build/hash/Hash.d.ts +1 -1
  13. package/build/hash/Hash.js +15 -18
  14. package/build/hash/Hash.js.map +1 -1
  15. package/build/sourcer/Expo.js +42 -44
  16. package/build/sourcer/Expo.js.map +1 -1
  17. package/build/sourcer/ExpoConfigLoader.d.ts +7 -0
  18. package/build/sourcer/ExpoConfigLoader.js +80 -0
  19. package/build/sourcer/ExpoConfigLoader.js.map +1 -0
  20. package/build/sourcer/Utils.d.ts +4 -0
  21. package/build/sourcer/Utils.js +38 -1
  22. package/build/sourcer/Utils.js.map +1 -1
  23. package/build/utils/Path.d.ts +5 -0
  24. package/build/utils/Path.js +26 -0
  25. package/build/utils/Path.js.map +1 -0
  26. package/e2e/__tests__/__snapshots__/managed-test.ts.snap +3 -2
  27. package/e2e/__tests__/bare-test.ts +7 -0
  28. package/e2e/__tests__/managed-test.ts +13 -3
  29. package/package.json +3 -3
  30. package/src/Fingerprint.ts +2 -2
  31. package/src/Fingerprint.types.ts +13 -1
  32. package/src/Options.ts +70 -7
  33. package/src/__tests__/Fingerprint-test.ts +43 -17
  34. package/src/hash/Hash.ts +20 -21
  35. package/src/hash/__tests__/Hash-test.ts +62 -16
  36. package/src/sourcer/Expo.ts +48 -55
  37. package/src/sourcer/ExpoConfigLoader.ts +84 -0
  38. package/src/sourcer/Utils.ts +39 -0
  39. package/src/sourcer/__tests__/Bare-test.ts +11 -5
  40. package/src/sourcer/__tests__/Expo-test.ts +88 -109
  41. package/src/sourcer/__tests__/PatchPackage-test.ts +6 -3
  42. package/src/sourcer/__tests__/Sourcer-test.ts +9 -2
  43. package/src/sourcer/__tests__/Utils-test.ts +41 -0
  44. package/src/utils/Path.ts +26 -0
  45. package/src/utils/__tests__/Path-test.ts +36 -0
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /**
3
+ * A helper script to load the Expo config and loaded plugins from a project
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.getExpoConfigLoaderPath = void 0;
10
+ const promises_1 = __importDefault(require("fs/promises"));
11
+ const module_1 = __importDefault(require("module"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const resolve_from_1 = __importDefault(require("resolve-from"));
14
+ const Options_1 = require("../Options");
15
+ const Path_1 = require("../utils/Path");
16
+ async function runAsync(programName, args = []) {
17
+ if (args.length < 1) {
18
+ console.log(`Usage: ${programName} <projectRoot> [ignoredFile]`);
19
+ return;
20
+ }
21
+ const projectRoot = path_1.default.resolve(args[0]);
22
+ const ignoredFile = args[1] ? path_1.default.resolve(args[1]) : null;
23
+ // @ts-expect-error: module internal _cache
24
+ const loadedModulesBefore = new Set(Object.keys(module_1.default._cache));
25
+ const { getConfig } = require((0, resolve_from_1.default)(path_1.default.resolve(projectRoot), 'expo/config'));
26
+ const config = await getConfig(projectRoot, { skipSDKVersionRequirement: true });
27
+ // @ts-expect-error: module internal _cache
28
+ const loadedModules = Object.keys(module_1.default._cache)
29
+ .filter((modulePath) => !loadedModulesBefore.has(modulePath))
30
+ .map((modulePath) => path_1.default.relative(projectRoot, modulePath));
31
+ const ignoredPaths = await loadIgnoredPathsAsync(ignoredFile);
32
+ const filteredLoadedModules = loadedModules.filter((modulePath) => !(0, Path_1.isIgnoredPath)(modulePath, ignoredPaths));
33
+ console.log(JSON.stringify({ config, loadedModules: filteredLoadedModules }));
34
+ }
35
+ // If running from the command line
36
+ if (require.main?.filename === __filename) {
37
+ (async () => {
38
+ const programIndex = process.argv.findIndex((arg) => arg === __filename);
39
+ try {
40
+ await runAsync(process.argv[programIndex], process.argv.slice(programIndex + 1));
41
+ }
42
+ catch (e) {
43
+ console.error('Uncaught Error', e);
44
+ process.exit(1);
45
+ }
46
+ })();
47
+ }
48
+ /**
49
+ * Load the generated ignored paths file from caller and remove the file after loading
50
+ */
51
+ async function loadIgnoredPathsAsync(ignoredFile) {
52
+ if (!ignoredFile) {
53
+ return Options_1.DEFAULT_IGNORE_PATHS;
54
+ }
55
+ const ignorePaths = [];
56
+ try {
57
+ const fingerprintIgnore = await promises_1.default.readFile(ignoredFile, 'utf8');
58
+ const fingerprintIgnoreLines = fingerprintIgnore.split('\n');
59
+ for (const line of fingerprintIgnoreLines) {
60
+ const trimmedLine = line.trim();
61
+ if (trimmedLine) {
62
+ ignorePaths.push(trimmedLine);
63
+ }
64
+ }
65
+ }
66
+ catch { }
67
+ try {
68
+ await promises_1.default.rm(ignoredFile);
69
+ }
70
+ catch { }
71
+ return ignorePaths;
72
+ }
73
+ /**
74
+ * Get the path to the ExpoConfigLoader file.
75
+ */
76
+ function getExpoConfigLoaderPath() {
77
+ return path_1.default.join(__dirname, 'ExpoConfigLoader.js');
78
+ }
79
+ exports.getExpoConfigLoaderPath = getExpoConfigLoaderPath;
80
+ //# sourceMappingURL=ExpoConfigLoader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoConfigLoader.js","sourceRoot":"","sources":["../../src/sourcer/ExpoConfigLoader.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,6 @@
1
1
  import type { HashSource } from '../Fingerprint.types';
2
2
  export declare function getFileBasedHashSourceAsync(projectRoot: string, filePath: string, reason: string): Promise<HashSource | null>;
3
+ /**
4
+ * A version of `JSON.stringify` that keeps the keys sorted
5
+ */
6
+ export declare function stringifyJsonSorted(target: any, space?: string | number | undefined): string;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getFileBasedHashSourceAsync = void 0;
6
+ exports.stringifyJsonSorted = exports.getFileBasedHashSourceAsync = void 0;
7
7
  const promises_1 = __importDefault(require("fs/promises"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  async function getFileBasedHashSourceAsync(projectRoot, filePath, reason) {
@@ -22,4 +22,41 @@ async function getFileBasedHashSourceAsync(projectRoot, filePath, reason) {
22
22
  return result;
23
23
  }
24
24
  exports.getFileBasedHashSourceAsync = getFileBasedHashSourceAsync;
25
+ /**
26
+ * A version of `JSON.stringify` that keeps the keys sorted
27
+ */
28
+ function stringifyJsonSorted(target, space) {
29
+ return JSON.stringify(target, (_, value) => sortJson(value), space);
30
+ }
31
+ exports.stringifyJsonSorted = stringifyJsonSorted;
32
+ function sortJson(json) {
33
+ if (Array.isArray(json)) {
34
+ return json.sort((a, b) => {
35
+ // Sort array items by their stringified value.
36
+ // We don't need the array to be sorted in meaningful way, just to be sorted in deterministic.
37
+ // E.g. `[{ b: '2' }, {}, { a: '3' }, null]` -> `[null, { a : '3' }, { b: '2' }, {}]`
38
+ // This result is not a perfect solution, but it's good enough for our use case.
39
+ const stringifiedA = stringifyJsonSorted(a);
40
+ const stringifiedB = stringifyJsonSorted(b);
41
+ if (stringifiedA < stringifiedB) {
42
+ return -1;
43
+ }
44
+ else if (stringifiedA > stringifiedB) {
45
+ return 1;
46
+ }
47
+ return 0;
48
+ });
49
+ }
50
+ if (json != null && typeof json === 'object') {
51
+ // Sort object items by keys
52
+ return Object.keys(json)
53
+ .sort()
54
+ .reduce((acc, key) => {
55
+ acc[key] = json[key];
56
+ return acc;
57
+ }, {});
58
+ }
59
+ // Return primitives
60
+ return json;
61
+ }
25
62
  //# sourceMappingURL=Utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Utils.js","sourceRoot":"","sources":["../../src/sourcer/Utils.ts"],"names":[],"mappings":";;;;;;AAAA,2DAA6B;AAC7B,gDAAwB;AAIjB,KAAK,UAAU,2BAA2B,CAC/C,WAAmB,EACnB,QAAgB,EAChB,MAAc;IAEd,IAAI,MAAM,GAAsB,IAAI,CAAC;IACrC,IAAI;QACF,MAAM,IAAI,GAAG,MAAM,kBAAE,CAAC,IAAI,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC7D,MAAM,GAAG;YACP,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM;YACzC,QAAQ;YACR,OAAO,EAAE,CAAC,MAAM,CAAC;SAClB,CAAC;KACH;IAAC,MAAM;QACN,MAAM,GAAG,IAAI,CAAC;KACf;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAjBD,kEAiBC"}
1
+ {"version":3,"file":"Utils.js","sourceRoot":"","sources":["../../src/sourcer/Utils.ts"],"names":[],"mappings":";;;;;;AAAA,2DAA6B;AAC7B,gDAAwB;AAIjB,KAAK,UAAU,2BAA2B,CAC/C,WAAmB,EACnB,QAAgB,EAChB,MAAc;IAEd,IAAI,MAAM,GAAsB,IAAI,CAAC;IACrC,IAAI;QACF,MAAM,IAAI,GAAG,MAAM,kBAAE,CAAC,IAAI,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC7D,MAAM,GAAG;YACP,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM;YACzC,QAAQ;YACR,OAAO,EAAE,CAAC,MAAM,CAAC;SAClB,CAAC;KACH;IAAC,MAAM;QACN,MAAM,GAAG,IAAI,CAAC;KACf;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAjBD,kEAiBC;AAED;;GAEG;AACH,SAAgB,mBAAmB,CAAC,MAAW,EAAE,KAAmC;IAClF,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;AACtE,CAAC;AAFD,kDAEC;AAED,SAAS,QAAQ,CAAC,IAAS;IACzB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QACvB,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACxB,+CAA+C;YAC/C,8FAA8F;YAC9F,qFAAqF;YACrF,gFAAgF;YAChF,MAAM,YAAY,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;YAC5C,MAAM,YAAY,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;YAC5C,IAAI,YAAY,GAAG,YAAY,EAAE;gBAC/B,OAAO,CAAC,CAAC,CAAC;aACX;iBAAM,IAAI,YAAY,GAAG,YAAY,EAAE;gBACtC,OAAO,CAAC,CAAC;aACV;YACD,OAAO,CAAC,CAAC;QACX,CAAC,CAAC,CAAC;KACJ;IAED,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;QAC5C,4BAA4B;QAC5B,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;aACrB,IAAI,EAAE;aACN,MAAM,CAAC,CAAC,GAAQ,EAAE,GAAW,EAAE,EAAE;YAChC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACrB,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAE,CAAC,CAAC;KACV;IAED,oBAAoB;IACpB,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,5 @@
1
+ import minimatch from 'minimatch';
2
+ /**
3
+ * Indicate the given `filePath` should be excluded by `ignorePaths`
4
+ */
5
+ export declare function isIgnoredPath(filePath: string, ignorePaths: string[], minimatchOptions?: minimatch.IOptions): boolean;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isIgnoredPath = void 0;
7
+ const minimatch_1 = __importDefault(require("minimatch"));
8
+ /**
9
+ * Indicate the given `filePath` should be excluded by `ignorePaths`
10
+ */
11
+ function isIgnoredPath(filePath, ignorePaths, minimatchOptions = { dot: true }) {
12
+ const minimatchObjs = ignorePaths.map((ignorePath) => new minimatch_1.default.Minimatch(ignorePath, minimatchOptions));
13
+ let result = false;
14
+ for (const minimatchObj of minimatchObjs) {
15
+ const currMatch = minimatchObj.match(filePath);
16
+ if (minimatchObj.negate && result && !currMatch) {
17
+ // Special handler for negate (!pattern).
18
+ // As long as previous match result is true and not matched from the current negate pattern, we should early return.
19
+ return false;
20
+ }
21
+ result || (result = currMatch);
22
+ }
23
+ return result;
24
+ }
25
+ exports.isIgnoredPath = isIgnoredPath;
26
+ //# sourceMappingURL=Path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Path.js","sourceRoot":"","sources":["../../src/utils/Path.ts"],"names":[],"mappings":";;;;;;AAAA,0DAAkC;AAElC;;GAEG;AACH,SAAgB,aAAa,CAC3B,QAAgB,EAChB,WAAqB,EACrB,mBAAuC,EAAE,GAAG,EAAE,IAAI,EAAE;IAEpD,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,CACnC,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,mBAAS,CAAC,SAAS,CAAC,UAAU,EAAE,gBAAgB,CAAC,CACtE,CAAC;IAEF,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE;QACxC,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC/C,IAAI,YAAY,CAAC,MAAM,IAAI,MAAM,IAAI,CAAC,SAAS,EAAE;YAC/C,yCAAyC;YACzC,oHAAoH;YACpH,OAAO,KAAK,CAAC;SACd;QACD,MAAM,KAAN,MAAM,GAAK,SAAS,EAAC;KACtB;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AApBD,sCAoBC"}
@@ -131,11 +131,12 @@ exports[`getHashSourcesAsync - managed project should match snapshot 1`] = `
131
131
  "type": "contents",
132
132
  },
133
133
  {
134
- "filePath": "app.json",
134
+ "contents": "{"android":{"adaptiveIcon":{"backgroundColor":"#FFFFFF","foregroundImage":"./assets/adaptive-icon.png"}},"assetBundlePatterns":["**/*"],"icon":"./assets/icon.png","ios":{"supportsTablet":true},"name":"fingerprint-e2e-managed","orientation":"portrait","platforms":["android","ios"],"sdkVersion":"47.0.0","slug":"fingerprint-e2e-managed","splash":{"backgroundColor":"#ffffff","image":"./assets/splash.png","resizeMode":"contain"},"updates":{"fallbackToCacheTimeout":0},"userInterfaceStyle":"light","version":"1.0.0","web":{"favicon":"./assets/favicon.png"}}",
135
+ "id": "expoConfig",
135
136
  "reasons": [
136
137
  "expoConfig",
137
138
  ],
138
- "type": "file",
139
+ "type": "contents",
139
140
  },
140
141
  {
141
142
  "filePath": "./assets/icon.png",
@@ -6,6 +6,13 @@ import rimraf from 'rimraf';
6
6
 
7
7
  import { createProjectHashAsync } from '../../src/Fingerprint';
8
8
 
9
+ jest.mock('../../src/sourcer/ExpoConfigLoader', () => ({
10
+ // Mock the getExpoConfigLoaderPath to use the built version rather than the typescript version from src
11
+ getExpoConfigLoaderPath: jest.fn(() =>
12
+ path.resolve(__dirname, '..', '..', 'build', 'sourcer', 'ExpoConfigLoader.js')
13
+ ),
14
+ }));
15
+
9
16
  describe('bare project test', () => {
10
17
  jest.setTimeout(600000);
11
18
  const tmpDir = os.tmpdir();
@@ -8,9 +8,16 @@ import {
8
8
  createProjectHashAsync,
9
9
  diffFingerprintChangesAsync,
10
10
  } from '../../src/Fingerprint';
11
- import { normalizeOptions } from '../../src/Options';
11
+ import { normalizeOptionsAsync } from '../../src/Options';
12
12
  import { getHashSourcesAsync } from '../../src/sourcer/Sourcer';
13
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
+
14
21
  describe('managed project test', () => {
15
22
  jest.setTimeout(600000);
16
23
  const tmpDir = require('temp-dir');
@@ -65,7 +72,7 @@ describe('managed project test', () => {
65
72
 
66
73
  const configPath = path.join(projectRoot, 'app.json');
67
74
  const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
68
- config.jsEngine = 'hermes';
75
+ config.expo.jsEngine = 'hermes';
69
76
  await fs.writeFile(configPath, JSON.stringify(config, null, 2));
70
77
 
71
78
  const hash2 = await createProjectHashAsync(projectRoot);
@@ -147,7 +154,10 @@ describe(`getHashSourcesAsync - managed project`, () => {
147
154
  });
148
155
 
149
156
  it('should match snapshot', async () => {
150
- const sources = await getHashSourcesAsync(projectRoot, normalizeOptions());
157
+ const sources = await getHashSourcesAsync(
158
+ projectRoot,
159
+ await normalizeOptionsAsync(projectRoot)
160
+ );
151
161
  expect(sources).toMatchSnapshot();
152
162
  });
153
163
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/fingerprint",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "A library to generate a fingerprint from a React Native project",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "clean": "rimraf build ./tsconfig.tsbuildinfo",
12
12
  "lint": "eslint .",
13
13
  "test": "jest",
14
- "test:e2e": "jest --config e2e/jest.config.js"
14
+ "test:e2e": "yarn prepare && jest --config e2e/jest.config.js"
15
15
  },
16
16
  "bin": {
17
17
  "fingerprint": "bin/cli.js"
@@ -48,5 +48,5 @@
48
48
  "glob": "^7.1.7",
49
49
  "temp-dir": "^2.0.0"
50
50
  },
51
- "gitHead": "1f5e9f77f22e5d6a464d34194236684a45fcf322"
51
+ "gitHead": "039366f050814a220cf31f87e0fbdf27416bb478"
52
52
  }
@@ -1,6 +1,6 @@
1
1
  import { dedupSources } from './Dedup';
2
2
  import type { Fingerprint, FingerprintSource, Options } from './Fingerprint.types';
3
- import { normalizeOptions } from './Options';
3
+ import { normalizeOptionsAsync } from './Options';
4
4
  import { sortSources } from './Sort';
5
5
  import { createFingerprintFromSourcesAsync } from './hash/Hash';
6
6
  import { getHashSourcesAsync } from './sourcer/Sourcer';
@@ -12,7 +12,7 @@ export async function createFingerprintAsync(
12
12
  projectRoot: string,
13
13
  options?: Options
14
14
  ): Promise<Fingerprint> {
15
- const opts = normalizeOptions(options);
15
+ const opts = await normalizeOptionsAsync(projectRoot, options);
16
16
  const sources = await getHashSourcesAsync(projectRoot, opts);
17
17
  const normalizedSources = sortSources(dedupSources(sources, projectRoot));
18
18
  const fingerprint = await createFingerprintFromSourcesAsync(normalizedSources, projectRoot, opts);
@@ -39,9 +39,21 @@ export interface Options {
39
39
  /**
40
40
  * Excludes directories from hashing. This supported pattern is as `glob()`.
41
41
  * Default is `['android/build', 'android/app/build', 'android/app/.cxx', 'ios/Pods']`.
42
+ * @deprecated Use `ignorePaths` instead.
42
43
  */
43
44
  dirExcludes?: string[];
44
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
+
45
57
  /**
46
58
  * Additional sources for hashing.
47
59
  */
@@ -54,7 +66,7 @@ export interface NormalizedOptions extends Options {
54
66
  platforms: NonNullable<Options['platforms']>;
55
67
  concurrentIoLimit: NonNullable<Options['concurrentIoLimit']>;
56
68
  hashAlgorithm: NonNullable<Options['hashAlgorithm']>;
57
- dirExcludes: NonNullable<Options['dirExcludes']>;
69
+ ignorePaths: NonNullable<Options['ignorePaths']>;
58
70
  }
59
71
 
60
72
  export interface HashSourceFile {
package/src/Options.ts CHANGED
@@ -1,18 +1,81 @@
1
+ import fs from 'fs/promises';
1
2
  import os from 'os';
3
+ import path from 'path';
2
4
 
3
5
  import type { NormalizedOptions, Options } from './Fingerprint.types';
4
6
 
5
- export function normalizeOptions(options?: Options): NormalizedOptions {
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> {
6
52
  return {
7
53
  ...options,
8
54
  platforms: options?.platforms ?? ['android', 'ios'],
9
55
  concurrentIoLimit: options?.concurrentIoLimit ?? os.cpus().length,
10
56
  hashAlgorithm: options?.hashAlgorithm ?? 'sha1',
11
- dirExcludes: options?.dirExcludes ?? [
12
- '**/android/build',
13
- '**/android/app/build',
14
- '**/android/app/.cxx',
15
- 'ios/Pods',
16
- ],
57
+ ignorePaths: await collectIgnorePathsAsync(projectRoot, options),
17
58
  };
18
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
+ }
@@ -2,7 +2,7 @@ import { vol } from 'memfs';
2
2
 
3
3
  import { createFingerprintAsync, diffFingerprintChangesAsync } from '../Fingerprint';
4
4
  import type { Fingerprint } from '../Fingerprint.types';
5
- import { normalizeOptions } from '../Options';
5
+ import { normalizeOptionsAsync } from '../Options';
6
6
 
7
7
  jest.mock('fs');
8
8
  jest.mock('fs/promises');
@@ -15,8 +15,12 @@ describe(diffFingerprintChangesAsync, () => {
15
15
 
16
16
  it('should return empty array when fingerprint matched', async () => {
17
17
  vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
18
- const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
19
- const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
18
+ const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
19
+ const diff = await diffFingerprintChangesAsync(
20
+ fingerprint,
21
+ '/app',
22
+ await normalizeOptionsAsync('/app')
23
+ );
20
24
  expect(diff.length).toBe(0);
21
25
  });
22
26
 
@@ -27,16 +31,21 @@ describe(diffFingerprintChangesAsync, () => {
27
31
  hash: '',
28
32
  };
29
33
 
30
- const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
34
+ const diff = await diffFingerprintChangesAsync(
35
+ fingerprint,
36
+ '/app',
37
+ await normalizeOptionsAsync('/app')
38
+ );
31
39
  expect(diff).toMatchInlineSnapshot(`
32
40
  [
33
41
  {
34
- "filePath": "app.json",
35
- "hash": "1fd2d92d50dc1da96b41795046b9ea4e30dd2b48",
42
+ "contents": "{"android":{"adaptiveIcon":{"backgroundColor":"#FFFFFF","foregroundImage":"./assets/adaptive-icon.png"}},"assetBundlePatterns":["**/*"],"icon":"./assets/icon.png","ios":{"supportsTablet":true},"name":"sdk47","orientation":"portrait","platforms":["android","ios","web"],"slug":"sdk47","splash":{"backgroundColor":"#ffffff","image":"./assets/splash.png","resizeMode":"contain"},"updates":{"fallbackToCacheTimeout":0},"userInterfaceStyle":"light","version":"1.0.0","web":{"favicon":"./assets/favicon.png"}}",
43
+ "hash": "33b2b95de3b0b474810630e51527a2c0a6e5de9c",
44
+ "id": "expoConfig",
36
45
  "reasons": [
37
46
  "expoConfig",
38
47
  ],
39
- "type": "file",
48
+ "type": "contents",
40
49
  },
41
50
  ]
42
51
  `);
@@ -46,19 +55,27 @@ describe(diffFingerprintChangesAsync, () => {
46
55
  vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
47
56
  const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8').toString());
48
57
  jest.doMock('/app/package.json', () => packageJson, { virtual: true });
49
- const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
58
+ const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
50
59
 
51
60
  // first round for bumping package version which should not cause changes
52
61
  packageJson.version = '111.111.111';
53
62
  jest.doMock('/app/package.json', () => packageJson, { virtual: true });
54
- let diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
63
+ let diff = await diffFingerprintChangesAsync(
64
+ fingerprint,
65
+ '/app',
66
+ await normalizeOptionsAsync('/app')
67
+ );
55
68
  expect(diff.length).toBe(0);
56
69
 
57
70
  // second round to update scripts section and it should cause changes
58
71
  packageJson.scripts ||= {};
59
72
  packageJson.scripts.postinstall = 'echo "hello"';
60
73
  jest.doMock('/app/package.json', () => packageJson, { virtual: true });
61
- diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
74
+ diff = await diffFingerprintChangesAsync(
75
+ fingerprint,
76
+ '/app',
77
+ await normalizeOptionsAsync('/app')
78
+ );
62
79
  jest.dontMock('/app/package.json');
63
80
  expect(diff).toMatchInlineSnapshot(`
64
81
  [
@@ -77,20 +94,25 @@ describe(diffFingerprintChangesAsync, () => {
77
94
 
78
95
  it('should return diff from file changes', async () => {
79
96
  vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
80
- const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
97
+ const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
81
98
  const config = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
82
99
  config.expo.jsEngine = 'jsc';
83
100
  vol.writeFileSync('/app/app.json', JSON.stringify(config, null, 2));
84
- const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
101
+ const diff = await diffFingerprintChangesAsync(
102
+ fingerprint,
103
+ '/app',
104
+ await normalizeOptionsAsync('/app')
105
+ );
85
106
  expect(diff).toMatchInlineSnapshot(`
86
107
  [
87
108
  {
88
- "filePath": "app.json",
89
- "hash": "9ff1b51ca9b9435e8b849bcc82e3900d70f0feee",
109
+ "contents": "{"android":{"adaptiveIcon":{"backgroundColor":"#FFFFFF","foregroundImage":"./assets/adaptive-icon.png"}},"assetBundlePatterns":["**/*"],"icon":"./assets/icon.png","ios":{"supportsTablet":true},"jsEngine":"jsc","name":"sdk47","orientation":"portrait","platforms":["android","ios","web"],"slug":"sdk47","splash":{"backgroundColor":"#ffffff","image":"./assets/splash.png","resizeMode":"contain"},"updates":{"fallbackToCacheTimeout":0},"userInterfaceStyle":"light","version":"1.0.0","web":{"favicon":"./assets/favicon.png"}}",
110
+ "hash": "7068a4234e7312c6ac54b776ea4dfad0ac789b2a",
111
+ "id": "expoConfig",
90
112
  "reasons": [
91
113
  "expoConfig",
92
114
  ],
93
- "type": "file",
115
+ "type": "contents",
94
116
  },
95
117
  ]
96
118
  `);
@@ -98,9 +120,13 @@ describe(diffFingerprintChangesAsync, () => {
98
120
 
99
121
  it('should return diff from dir changes', async () => {
100
122
  vol.fromJSON(require('../sourcer/__tests__/fixtures/BareReactNative70Project.json'));
101
- const fingerprint = await createFingerprintAsync('/app', normalizeOptions());
123
+ const fingerprint = await createFingerprintAsync('/app', await normalizeOptionsAsync('/app'));
102
124
  vol.writeFileSync('/app/ios/README.md', '# Adding new file in ios dir');
103
- const diff = await diffFingerprintChangesAsync(fingerprint, '/app', normalizeOptions());
125
+ const diff = await diffFingerprintChangesAsync(
126
+ fingerprint,
127
+ '/app',
128
+ await normalizeOptionsAsync('/app')
129
+ );
104
130
  expect(diff).toMatchInlineSnapshot(`
105
131
  [
106
132
  {
package/src/hash/Hash.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { createReadStream } from 'fs';
3
3
  import fs from 'fs/promises';
4
- import minimatch from 'minimatch';
5
4
  import pLimit from 'p-limit';
6
5
  import path from 'path';
7
6
 
@@ -13,6 +12,7 @@ import type {
13
12
  HashSourceContents,
14
13
  NormalizedOptions,
15
14
  } from '../Fingerprint.types';
15
+ import { isIgnoredPath } from '../utils/Path';
16
16
  import { profile } from '../utils/Profile';
17
17
 
18
18
  /**
@@ -82,10 +82,14 @@ export async function createFileHashResultsAsync(
82
82
  limiter: pLimit.Limit,
83
83
  projectRoot: string,
84
84
  options: NormalizedOptions
85
- ): Promise<HashResult> {
85
+ ): Promise<HashResult | null> {
86
86
  // Backup code for faster hashing
87
87
  /*
88
88
  return limiter(async () => {
89
+ if (isIgnoredPath(filePath, options.ignorePaths)) {
90
+ return null;
91
+ }
92
+
89
93
  const hasher = createHash(options.hashAlgorithm);
90
94
 
91
95
  const stat = await fs.stat(filePath);
@@ -102,7 +106,11 @@ export async function createFileHashResultsAsync(
102
106
  */
103
107
 
104
108
  return limiter(() => {
105
- return new Promise<HashResult>((resolve, reject) => {
109
+ return new Promise<HashResult | null>((resolve, reject) => {
110
+ if (isIgnoredPath(filePath, options.ignorePaths)) {
111
+ return resolve(null);
112
+ }
113
+
106
114
  let resolved = false;
107
115
  const hasher = createHash(options.hashAlgorithm);
108
116
  const stream = createReadStream(path.join(projectRoot, filePath));
@@ -123,18 +131,6 @@ export async function createFileHashResultsAsync(
123
131
  });
124
132
  }
125
133
 
126
- /**
127
- * Indicate the given `dirPath` should be excluded by `dirExcludes`
128
- */
129
- function isExcludedDir(dirPath: string, dirExcludes: string[]): boolean {
130
- for (const exclude of dirExcludes) {
131
- if (minimatch(dirPath, exclude)) {
132
- return true;
133
- }
134
- }
135
- return false;
136
- }
137
-
138
134
  /**
139
135
  * Create `HashResult` for a dir.
140
136
  * If the dir is excluded, returns null rather than a HashResult
@@ -146,7 +142,7 @@ export async function createDirHashResultsAsync(
146
142
  options: NormalizedOptions,
147
143
  depth: number = 0
148
144
  ): Promise<HashResult | null> {
149
- if (isExcludedDir(dirPath, options.dirExcludes)) {
145
+ if (isIgnoredPath(dirPath, options.ignorePaths)) {
150
146
  return null;
151
147
  }
152
148
  const dirents = (await fs.readdir(path.join(projectRoot, dirPath), { withFileTypes: true })).sort(
@@ -164,12 +160,15 @@ export async function createDirHashResultsAsync(
164
160
  }
165
161
 
166
162
  const hasher = createHash(options.hashAlgorithm);
167
- const results = await Promise.all(promises);
163
+ const results = (await Promise.all(promises)).filter(
164
+ (result): result is HashResult => result != null
165
+ );
166
+ if (results.length === 0) {
167
+ return null;
168
+ }
168
169
  for (const result of results) {
169
- if (result != null) {
170
- hasher.update(result.id);
171
- hasher.update(result.hex);
172
- }
170
+ hasher.update(result.id);
171
+ hasher.update(result.hex);
173
172
  }
174
173
  const hex = hasher.digest('hex');
175
174