From f529fd6ba590101a3dfa710a92befb81994ed2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Wed, 11 Jun 2025 09:06:39 -0700 Subject: [PATCH] Add snapshot validation to build-types (#51893) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51893 This diff adds `--validate` flag that runs snapshot validation to determine if the `ReactNativeApi.d.ts` rollup has been changed (if JS public API has been touched). There was also an issue with `sortProperties` that reordered some properties (ex. in ImagePropsBase) after removing one of them (ex. accessible) which had negative impact on the displayed result. ### Motivation Compare previous snapshot with the one built on the current revision to determine the impact of made changes on the public API surface. Display differences in human readable format using `diff` method from the `jest-diff` library. For now `--validate` flag is not useful on its own. It should be used with `--withSnapshot` flag (which will be removed shortly and generating snapshot will be a default mechanism). Changelog: [General][Added] - Add `--validate` flag to `build-types` script for JS API snapshot validation. Reviewed By: huntie Differential Revision: D76135158 fbshipit-source-id: 53f5b142c66e3e3931961f741c3f2fab8ccdc228 --- scripts/build-types/BuildApiSnapshot.js | 51 ++++++++++++++++++++++--- scripts/build-types/index.js | 5 ++- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/scripts/build-types/BuildApiSnapshot.js b/scripts/build-types/BuildApiSnapshot.js index b70d4dff1e0..fe0c7b14ce6 100644 --- a/scripts/build-types/BuildApiSnapshot.js +++ b/scripts/build-types/BuildApiSnapshot.js @@ -23,9 +23,11 @@ const { } = require('@microsoft/api-extractor'); const {promises: fs} = require('fs'); const glob = require('glob'); +const {diff} = require('jest-diff'); const path = require('path'); const prettier = require('prettier'); const osTempDir = require('temp-dir'); +const {styleText} = require('util'); const inputFilesPostTransforms: $ReadOnlyArray> = [ require('./transforms/renameDefaultExportedIdentifiers'), @@ -37,7 +39,7 @@ const postTransforms: $ReadOnlyArray> = [ require('./transforms/sortUnions'), ]; -async function buildAPISnapshot() { +async function buildAPISnapshot(validate: boolean) { const tempDirectory = await createTempDir('react-native-js-api-snapshot'); const packages = await findPackagesWithTypedef(); @@ -56,12 +58,23 @@ async function buildAPISnapshot() { if (extractorResult.succeeded) { const apiSnapshot = apiSnapshotTemplate( await getCleanedUpRollup(tempDirectory), - ); + ) as string; - await fs.writeFile( - path.join(REACT_NATIVE_PACKAGE_DIR, 'ReactNativeApi.d.ts'), - apiSnapshot, - ); + if (validate) { + const prevSnapshot = await fs.readFile( + path.join(REACT_NATIVE_PACKAGE_DIR, 'ReactNativeApi.d.ts'), + 'utf-8', + ); + const hasChanged = await validateSnapshots(prevSnapshot, apiSnapshot); + if (hasChanged) { + process.exitCode = 1; + } + } else { + await fs.writeFile( + path.join(REACT_NATIVE_PACKAGE_DIR, 'ReactNativeApi.d.ts'), + apiSnapshot, + ); + } } else { process.exitCode = 1; console.error( @@ -73,6 +86,32 @@ async function buildAPISnapshot() { await fs.rm(tempDirectory, {recursive: true}); } +async function validateSnapshots( + prevSnapshot: string, + newSnapshot: string, +): Promise { + const hasChanged = newSnapshot !== prevSnapshot; + if (hasChanged) { + const options = { + aAnnotation: 'Previous Snapshot', + bAnnotation: 'New Snapshot', + expand: false, + emptyFirstOrLastLinePlaceholder: '↵', + includeChangeCounts: true, + aColor: (line: string) => styleText(['red'], line), + bColor: (line: string) => styleText(['green'], line), + }; + + const diffResult = diff(prevSnapshot, newSnapshot, options); + console.error( + `\n${styleText(['inverse'], ' VALIDATE ')} ReactNativeApi.d.ts has changed. Please re-run \`yarn build-types\` and commit the updated snapshot.\n\n`, + diffResult, + ); + } + + return hasChanged; +} + async function findPackagesWithTypedef() { const packagesWithGeneratedTypes = glob .sync(`${PACKAGES_DIR}/**/types_generated`, {nodir: false}) diff --git a/scripts/build-types/index.js b/scripts/build-types/index.js index 388226c7069..cee92641066 100644 --- a/scripts/build-types/index.js +++ b/scripts/build-types/index.js @@ -20,12 +20,13 @@ const config = { debug: {type: 'boolean'}, help: {type: 'boolean'}, withSnapshot: {type: 'boolean'}, + validate: {type: 'boolean'}, }, }; async function main() { const { - values: {debug: debugEnabled, help, withSnapshot}, + values: {debug: debugEnabled, help, withSnapshot, validate}, /* $FlowFixMe[incompatible-call] Natural Inference rollout. See * https://fburl.com/workplace/6291gfvu */ } = parseArgs(config); @@ -68,7 +69,7 @@ async function main() { '\n', ); - await buildApiSnapshot(); + await buildApiSnapshot(validate); } }