diff --git a/__tests__/releaseVersionPatterns.test.ts b/__tests__/releaseVersionPatterns.test.ts new file mode 100644 index 000000000..68ef19536 --- /dev/null +++ b/__tests__/releaseVersionPatterns.test.ts @@ -0,0 +1,115 @@ +// +// releaseVersionPatterns.test.ts +// react-native-purchases +// +// Created by Antonio Pallares. +// + +import * as fs from 'fs'; +import * as path from 'path'; + +const repoRoot = path.resolve(__dirname, '..'); + +const read = (relativePath: string): string => + fs.readFileSync(path.join(repoRoot, relativePath), 'utf8'); + +const versionCapture = '(\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.]+)*)'; + +const escapeForRegExp = (value: string): string => + value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** + * Turns a Fastfile pattern such as `packageVersion = '{x}'` into a regular + * expression that captures whatever version the file currently holds. + */ +const patternToRegExp = (pattern: string): RegExp => + new RegExp(pattern.split('{x}').map(escapeForRegExp).join(versionCapture)); + +/** + * Parses one of the Fastfile's `{ 'path' => ['pattern with {x}'] }` hashes. + * + * The lanes feed these to `replace_in`, which substitutes `{x}` and does a + * literal `String#gsub`. A pattern matching nothing is skipped silently, so a + * wrong pattern leaves its file un-bumped without ever failing a release. + */ +const parseFastfileHash = (hashName: string): Map => { + const fastfile = read('fastlane/Fastfile'); + const block = fastfile.match(new RegExp(`${hashName} = \\{([\\s\\S]*?)\\n\\}`)); + if (!block) { + throw new Error(`Could not find ${hashName} in the Fastfile`); + } + + const unescapeRuby = (value: string): string => value.replace(/\\(['\\])/g, '$1'); + const entries = new Map(); + + // Arrays may span several lines, so this cannot be parsed line by line. + const entryPattern = /'((?:[^'\\]|\\.)*)'\s*=>\s*\[([\s\S]*?)\]/g; + for (const entry of Array.from(block[1].matchAll(entryPattern))) { + const patterns = Array.from(entry[2].matchAll(/'((?:[^'\\]|\\.)*)'/g)).map((match) => + unescapeRuby(match[1]) + ); + entries.set(unescapeRuby(entry[1]), patterns); + } + + // Guards against the parser losing coverage if the hash is ever reformatted + // into a shape it does not understand -- the same silent skip this suite exists + // to catch. + const declaredKeys = (block[1].match(/^\s*'(?:[^'\\]|\\.)*'\s*=>/gm) || []).length; + if (entries.size !== declaredKeys) { + throw new Error( + `Parsed ${entries.size} of ${declaredKeys} entries in ${hashName}; the parser needs updating` + ); + } + + return entries; +}; + +interface PatternMatch { + location: string; + version: string | null; +} + +const matchAllPatterns = (hashName: string): PatternMatch[] => + Array.from(parseFastfileHash(hashName)) + .flatMap(([relativePath, patterns]) => { + const contents = read(relativePath); + return patterns.map((pattern) => { + const match = contents.match(patternToRegExp(pattern)); + return { + location: `${relativePath}: ${pattern}`, + // Every `{x}` in a pattern must resolve to the same version. + version: match ? (new Set(match.slice(1)).size === 1 ? match[1] : 'inconsistent') : null, + }; + }); + }); + +describe.each(['files_with_version_number', 'files_to_update_phc_version'])( + '%s', + (hashName) => { + const matches = matchAllPatterns(hashName); + + it('covers a non-trivial number of files', () => { + expect(matches.length).toBeGreaterThan(5); + }); + + it('has no pattern that matches nothing', () => { + const dead = matches.filter((match) => match.version === null).map((match) => match.location); + + expect(dead).toEqual([]); + }); + + it('finds the same version everywhere', () => { + const versions = new Map(); + for (const match of matches) { + if (match.version === null) { + continue; + } + versions.set(match.version, [...(versions.get(match.version) || []), match.location]); + } + + expect(Object.fromEntries(versions)).toEqual({ + [Array.from(versions.keys())[0]]: expect.any(Array), + }); + }); + } +); diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 36237f790..5a3222919 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -4,7 +4,6 @@ files_with_version_number = { './yarn.lock' => [ 'react-native-purchases: {x}', 'react-native-purchases-store-galaxy: {x}', - '"react-native-purchases@{x},', '"react-native-purchases-store-galaxy@{x},', ], './react-native-purchases-store-galaxy/package.json' => ['"version": "{x}"', '"react-native-purchases": "{x}"'], @@ -15,12 +14,12 @@ files_with_version_number = { './android/build.gradle' => ['versionName \'{x}\''], './react-native-purchases-ui/android/build.gradle' => ['versionName \'{x}\''], './android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java' => ['PLUGIN_VERSION = "{x}"'], - './src/browser/nativeModule.ts' => ['packageVersion = "{x}"'] + './src/browser/nativeModule.ts' => ['packageVersion = \'{x}\''] } files_to_update_phc_version = { 'RNPurchases.podspec' => ['"PurchasesHybridCommon", \'{x}\''], 'react-native-purchases-store-galaxy/android/build.gradle' => ['com.revenuecat.purchases:purchases-hybrid-common-store-galaxy:{x}'], - 'react-native-purchases-ui/RNPaywalls.podspec' => ['"PurchasesHybridCommon", \'{x}\'', '"PurchasesHybridCommonUI", \'{x}\''], + 'react-native-purchases-ui/RNPaywalls.podspec' => ['"PurchasesHybridCommonUI", \'{x}\''], 'android/build.gradle' => ['com.revenuecat.purchases:purchases-hybrid-common:{x}'], 'react-native-purchases-ui/android/build.gradle' => ['com.revenuecat.purchases:purchases-hybrid-common-ui:{x}'], 'package.json' => ['"@revenuecat/purchases-typescript-internal": "{x}"', '"@revenuecat/purchases-js-hybrid-mappings": "{x}"'], diff --git a/src/browser/nativeModule.ts b/src/browser/nativeModule.ts index c865ae00b..b2d09e7bc 100644 --- a/src/browser/nativeModule.ts +++ b/src/browser/nativeModule.ts @@ -9,7 +9,7 @@ import { ensurePurchasesConfigured, methodNotSupportedOnWeb } from './utils'; import { purchaseSimulatedPackage } from './simulatedstore/purchaseSimulatedPackageHelper'; -const packageVersion = '9.1.0'; +const packageVersion = '10.7.2'; /** * Browser implementation of the native module. This will be used in the browser and Expo Go.