Update JS API snapshot to group exports in single block (#52235)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52235

Adds `organizeDeclarations` transform, replacing `sortTypeDefinitions`.

All `export declare ...` statements are now collected and represented at the end of the snapshot in a single `export {}` block — significantly improving readability and diffing.

Changelog: [Internal]

Reviewed By: j-piasecki

Differential Revision: D77150017

fbshipit-source-id: 1bd451c0e2a18fd6fc0504970b10a5d2502ac872
This commit is contained in:
Alex Hunt
2025-06-25 02:55:42 -07:00
committed by Facebook GitHub Bot
parent 08a59c59e0
commit ece8ca82cd
6 changed files with 110 additions and 57 deletions
+1 -1
View File
@@ -36,10 +36,10 @@ const inputFilesPostTransforms: $ReadOnlyArray<PluginObj<mixed>> = [
const postTransforms: $ReadOnlyArray<PluginObj<mixed>> = [
require('./transforms/typescript/stripUnstableApis'),
require('./transforms/typescript/sortTypeDefinitions'),
require('./transforms/typescript/sortProperties'),
require('./transforms/typescript/sortUnions'),
require('./transforms/removeUndefinedFromOptionalMembers'),
require('./transforms/typescript/organizeDeclarations'),
];
async function buildAPISnapshot(validate: boolean) {
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sortTypeDefinitions should divide top-lelvel declarations into exported and non-exported sections 1`] = `
exports[`organizeDeclarations should sort declarations and move exports into single export block 1`] = `
"/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
@@ -9,16 +9,17 @@ exports[`sortTypeDefinitions should divide top-lelvel declarations into exported
*/
import * as React from \\"react\\";
export declare type ExportedFoo = \\"A\\" | \\"B\\";
export declare type ExportedBar = null | string | number;
export declare class ExportedBaz {
foo: string;
}
export declare function exportedFn(): void;
declare type Foo = \\"A\\" | \\"B\\";
declare type Bar = null | string | number;
declare class Baz {
foo: string;
}
declare function fn(): void;"
declare type ExportedBar = null | string | number;
declare class ExportedBaz {
foo: string;
}
declare function exportedFn(): void;
declare type ExportedFoo = \\"A\\" | \\"B\\";
declare function fn(): void;
declare type Foo = \\"A\\" | \\"B\\";
export { ExportedBar, ExportedBaz, ExportedFoo, exportedFn };"
`;
@@ -8,23 +8,23 @@
* @format
*/
const sortTypeDefinitionsVisitor = require('../sortTypeDefinitions');
const organizeDeclarations = require('../organizeDeclarations');
const babel = require('@babel/core');
const {promises: fs} = require('fs');
const path = require('path');
async function translate(code: string): Promise<string> {
const result = await babel.transformAsync(code, {
plugins: ['@babel/plugin-syntax-typescript', sortTypeDefinitionsVisitor],
plugins: ['@babel/plugin-syntax-typescript', organizeDeclarations],
});
return result.code;
}
describe('sortTypeDefinitions', () => {
test('should divide top-lelvel declarations into exported and non-exported sections', async () => {
describe('organizeDeclarations', () => {
test('should sort declarations and move exports into single export block', async () => {
const code = await fs.readFile(
path.join(__dirname, '../__fixtures__/sortTypeDefinitions.d.ts'),
path.join(__dirname, '../__fixtures__/organizeDeclarations.d.ts'),
'utf-8',
);
const result = await translate(code);
@@ -0,0 +1,94 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {PluginObj} from '@babel/core';
import * as t from '@babel/types';
/**
* A visitor that organizes top level type declarations into our desired API
* snapshot format.
*
* - Sorts declarations alphabetically.
* - Moves all exports into a single block at the end of the file.
*/
const visitor: PluginObj<mixed> = {
visitor: {
Program(path) {
const exportedIdentifiers: Set<string> = new Set();
// Collect exported identifiers
path.get('body').forEach(nodePath => {
if (nodePath.isExportNamedDeclaration()) {
if (nodePath.node.declaration) {
const declaration = nodePath.node.declaration;
if (
t.isTSInterfaceDeclaration(declaration) ||
t.isTSTypeAliasDeclaration(declaration) ||
t.isFunctionDeclaration(declaration) ||
t.isClassDeclaration(declaration) ||
t.isVariableDeclaration(declaration) ||
t.isTSDeclareFunction(declaration) ||
t.isTSModuleDeclaration(declaration) ||
t.isTSEnumDeclaration(declaration)
) {
if (declaration.id && declaration.id.name != null) {
exportedIdentifiers.add(declaration.id.name);
} else if (
t.isVariableDeclaration(declaration) &&
declaration.declarations.length > 0
) {
declaration.declarations.forEach(declarator => {
if (t.isIdentifier(declarator.id)) {
exportedIdentifiers.add(declarator.id.name);
}
});
}
nodePath.replaceWith(declaration); // Remove export
} else {
throw new Error(
`Unexpected declaration type for top-level export: ${declaration.type}`,
);
}
} else if (nodePath.node.specifiers) {
nodePath.node.specifiers.forEach(specifier => {
if (specifier.type === 'ExportSpecifier') {
exportedIdentifiers.add(specifier.local.name);
}
});
nodePath.remove(); // Remove export statement
}
}
});
// Sort declarations alphabetically
path.node.body.sort((a, b) => {
const aName = a.id?.name || '';
const bName = b.id?.name || '';
return aName.localeCompare(bName);
});
// Move all exports into single `export {}` block
if (exportedIdentifiers.size > 0) {
const sortedIdentifiers = Array.from(exportedIdentifiers).sort();
const exportStatement = t.exportNamedDeclaration(
// $FlowIgnore[incompatible-call]
null,
sortedIdentifiers.map(name =>
t.exportSpecifier(t.identifier(name), t.identifier(name)),
),
);
path.pushContainer('body', exportStatement);
}
},
},
};
module.exports = visitor;
@@ -1,42 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {PluginObj} from '@babel/core';
import * as t from '@babel/types';
const visitor: PluginObj<mixed> = {
visitor: {
Program(path) {
path.node.body.sort((a, b) => {
// Push import declarations at the top of the file
if (t.isImportDeclaration(a) && !t.isImportDeclaration(b)) {
return -1;
} else if (!t.isImportDeclaration(a) && t.isImportDeclaration(b)) {
return 1;
}
const aExported = t.isExportNamedDeclaration(a);
const bExported = t.isExportNamedDeclaration(b);
// Push exported declarations before non-exported declarations
if (aExported && !bExported) {
return -1;
} else if (!aExported && bExported) {
return 1;
}
return 0;
});
},
},
};
module.exports = visitor;