diff --git a/scripts/build-types/BuildApiSnapshot.js b/scripts/build-types/BuildApiSnapshot.js index 393ff4f8c5f..df9e6c2f3ac 100644 --- a/scripts/build-types/BuildApiSnapshot.js +++ b/scripts/build-types/BuildApiSnapshot.js @@ -35,16 +35,25 @@ const inputFilesPostTransforms: $ReadOnlyArray> = [ require('./transforms/typescript/stripUnstableApis'), ]; -const postTransforms: $ReadOnlyArray> = [ +const postTransforms = ( + options: BuildApiSnapshotOptions, +): $ReadOnlyArray> => [ require('./transforms/typescript/simplifyTypes'), require('./transforms/typescript/sortProperties'), require('./transforms/typescript/sortUnions'), require('./transforms/typescript/removeUndefinedFromOptionalMembers'), require('./transforms/typescript/organizeDeclarations'), - require('./transforms/typescript/versionExportedApis'), + require('./transforms/typescript/versionExportedApis')( + options.debugVersionAnnotations, + ), ]; -async function buildAPISnapshot(validate: boolean) { +type BuildApiSnapshotOptions = $ReadOnly<{ + validate: boolean, + debugVersionAnnotations: boolean, +}>; + +async function buildAPISnapshot(options: BuildApiSnapshotOptions) { console.log( styleText('yellow', ' >') + ' Creating temp dir for api-extractor', ); @@ -76,7 +85,7 @@ async function buildAPISnapshot(validate: boolean) { console.log(styleText('yellow', ' >') + ' Applying additional transforms'); const apiSnapshot = apiSnapshotTemplate( - await getProcessedSnapshotResult(tempDirectory), + await getProcessedSnapshotResult(tempDirectory, options), ) as string; console.log(styleText('yellow', ' >') + ' Removing temp dir'); @@ -87,7 +96,7 @@ async function buildAPISnapshot(validate: boolean) { 'ReactNativeApi.d.ts', ); - if (validate) { + if (options.validate) { console.log( '\n' + styleText( @@ -230,6 +239,7 @@ async function rewriteLocalImports( async function getProcessedSnapshotResult( tempDirectory: string, + options: BuildApiSnapshotOptions, ): Promise { const rollupPath = path.join( tempDirectory, @@ -247,7 +257,7 @@ async function getProcessedSnapshotResult( const transformedRollup = await applyBabelTransformsSeq( cleanedRollup, - postTransforms, + postTransforms(options), ); return prettier diff --git a/scripts/build-types/index.js b/scripts/build-types/index.js index 087a585259b..989a9b17f8a 100644 --- a/scripts/build-types/index.js +++ b/scripts/build-types/index.js @@ -18,6 +18,7 @@ const {parseArgs, styleText} = require('util'); const config = { options: { debug: {type: 'boolean'}, + 'debug-version-annotations': {type: 'boolean'}, help: {type: 'boolean'}, withSnapshot: {type: 'boolean'}, validate: {type: 'boolean'}, @@ -26,7 +27,13 @@ const config = { async function main() { const { - values: {debug: debugEnabled, help, withSnapshot, validate}, + values: { + debug: debugEnabled, + 'debug-version-annotations': debugVersionAnnotations, + help, + withSnapshot, + validate, + }, /* $FlowFixMe[incompatible-call] Natural Inference rollout. See * https://fburl.com/workplace/6291gfvu */ } = parseArgs(config); @@ -39,6 +46,9 @@ async function main() { Options: --debug Enable debug logging. + --debug-version-annotations + Outputs debug info alongside versioned type hashes as + part of the API snapshot contents. --withSnapshot [Experimental] Include API snapshot generation. --validate Validate if the current API snapshot on disk is up to date. Exits with an error if differences are detected. @@ -74,7 +84,7 @@ async function main() { styleText(['bold', 'inverse'], ' [Experimental] Building API snapshot ') + '\n', ); - await buildApiSnapshot(validate); + await buildApiSnapshot({validate, debugVersionAnnotations}); } } diff --git a/scripts/build-types/transforms/typescript/versionExportedApis.js b/scripts/build-types/transforms/typescript/versionExportedApis.js index 94bd9a27b61..532b75ebc38 100644 --- a/scripts/build-types/transforms/typescript/versionExportedApis.js +++ b/scripts/build-types/transforms/typescript/versionExportedApis.js @@ -27,244 +27,285 @@ const debug = require('debug')('build-types:transforms:versionExportedApis'); * The approach also allows this implementation to be updated in future * without causing structural changes elsewhere in the API snapshot. */ -const visitor: PluginObj = { - visitor: { - Program(path) { - const declarations = new Map(); - const dependencyGraph = new Map>(); - const computedHashes = new Map(); +function createVersionExportedApis( + outputDebugAnnotations: boolean = false, +): PluginObj { + return { + visitor: { + Program(path) { + const declarations = new Map(); + const dependencyGraph = new Map>(); + const computedHashes = new Map(); - // Collect all type declarations and build dependency graph - for (const nodePath of path.get('body')) { - const node = nodePath.node; - const typeName = node.id?.name; - if ( - (t.isTSDeclareFunction(node) || - t.isTSTypeAliasDeclaration(node) || - t.isTSInterfaceDeclaration(node) || - t.isTSEnumDeclaration(node) || - t.isClassDeclaration(node) || - t.isTSModuleDeclaration(node)) && - typeName != null - ) { - declarations.set(typeName, node); - dependencyGraph.set( - typeName, - Array.from(getTypeReferencesForNode(node)), - ); - } - } - - // Helper to recursively collect all dependencies for a type - const getAllDependencies = ( - typeName: string, - visited: Set = new Set(), - depth: number = 0, - ): Set => { - if (visited.has(typeName)) { - return visited; - } - - visited.add(typeName); - const directDeps = dependencyGraph.get(typeName) || []; - - const indent = ' '.repeat(depth); - - for (const dep of directDeps) { - if (declarations.has(dep) && !visited.has(dep)) { - debug(`${indent}- Found dependency: ${dep}`); - getAllDependencies(dep, visited, depth + 1); - } else if (!declarations.has(dep)) { - debug(`${indent}- External dependency: ${dep}`); + // Collect all type declarations and build dependency graph + for (const nodePath of path.get('body')) { + const node = nodePath.node; + const typeName = node.id?.name; + if ( + (t.isTSDeclareFunction(node) || + t.isTSTypeAliasDeclaration(node) || + t.isTSInterfaceDeclaration(node) || + t.isTSEnumDeclaration(node) || + t.isClassDeclaration(node) || + t.isTSModuleDeclaration(node)) && + typeName != null + ) { + declarations.set(typeName, node); + dependencyGraph.set( + typeName, + Array.from(getTypeReferencesForNode(node)), + ); } } - return visited; - }; - - // Helper to generate a stable hash for a type and all local dependencies - const generateTypeHash = (typeName: string): string => { - const cached = computedHashes.get(typeName); - if (cached != null) { - return cached; - } - - debug(`\n[GENERATE HASH] Analyzing dependencies for ${typeName}`); - const allDeps = getAllDependencies(typeName); - allDeps.delete(typeName); // Remove self from dependencies - - const sortedDeps = Array.from(allDeps).sort(); - const hasher = createHash('sha256'); - - // Add the type's own code to the hash - const typeDecl = declarations.get(typeName); - if (typeDecl) { - const {code} = generate(typeDecl); - hasher.update(code); - debug(`\n[HASH INPUT] Type ${typeName}:\n${code}\n`); - } - - // Add code for each dependency to the hash - for (const dep of sortedDeps) { - const depDecl = declarations.get(dep); - if (depDecl) { - const {code} = generate(depDecl); - hasher.update(code); - debug(`[HASH INPUT] Dependency ${dep} for ${typeName}:\n${code}\n`); + // Helper to recursively collect all dependencies for a type + const getAllDependencies = ( + typeName: string, + visited: Set = new Set(), + depth: number = 0, + ): Set => { + if (visited.has(typeName)) { + return visited; } - } - const hash = hasher.digest('hex').slice(0, 8); - debug(`[HASH RESULT] ${typeName}: ${hash}`); - computedHashes.set(typeName, hash); - return hash; - }; + visited.add(typeName); + const directDeps = dependencyGraph.get(typeName) || []; - // Process export block and annotate with dependencies hash - for (const nodePath of path.get('body')) { - if ( - t.isExportNamedDeclaration(nodePath.node) && - !nodePath.node.declaration && - nodePath.node.specifiers != null - ) { - const specifiers = nodePath.node.specifiers.map(specifier => { - // $FlowIgnore[incompatible-type] nodePath is refined above - // $FlowIgnore[incompatible-use] - const name: string = specifier.exported.name; - if (declarations.has(name)) { - const hash = generateTypeHash(name); - return t.addComment(specifier, 'trailing', ` ${hash}`, true); + const indent = ' '.repeat(depth); + + for (const dep of directDeps) { + if (declarations.has(dep) && !visited.has(dep)) { + debug(`${indent}- Found dependency: ${dep}`); + getAllDependencies(dep, visited, depth + 1); + } else if (!declarations.has(dep)) { + debug(`${indent}- External dependency: ${dep}`); } - return specifier; - }); - // $FlowIgnore[prop-missing] - // $FlowIgnore[incompatible-type] - nodePath.node.specifiers = specifiers; + } + + return visited; + }; + + // Helper to generate a stable hash for a type and all local dependencies + const generateTypeHash = (typeName: string): string => { + const cached = computedHashes.get(typeName); + if (cached != null) { + return cached; + } + + debug(`\n[GENERATE HASH] Analyzing dependencies for ${typeName}`); + const allDeps = getAllDependencies(typeName); + allDeps.delete(typeName); // Remove self from dependencies + + const sortedDeps = Array.from(allDeps).sort(); + const hasher = createHash('sha256'); + + // Add the type's own code to the hash + const typeDecl = declarations.get(typeName); + if (typeDecl) { + const {code} = generate(typeDecl); + hasher.update(code); + debug(`\n[HASH INPUT] Type ${typeName}:\n${code}\n`); + } + + // Add code for each dependency to the hash + for (const dep of sortedDeps) { + const depDecl = declarations.get(dep); + if (depDecl) { + const {code} = generate(depDecl); + hasher.update(code); + debug( + `[HASH INPUT] Dependency ${dep} for ${typeName}:\n${code}\n`, + ); + } + } + + const hash = hasher.digest('hex').slice(0, 8); + debug(`[HASH RESULT] ${typeName}: ${hash}`); + computedHashes.set(typeName, hash); + return hash; + }; + + // Helper to get a compact representation of the dependency tree + const getCompactDependencyTree = ( + typeName: string, + maxDepth: number = 2, + ): string => { + const result = []; + const visited = new Set(); + const traverse = (name: string, depth: number = 0): void => { + if (depth > maxDepth || visited.has(name)) { + return; + } + visited.add(name); + const deps = dependencyGraph.get(name) || []; + const internalDeps = deps.filter(dep => declarations.has(dep)); + if (internalDeps.length > 0) { + result.push(`${name}→[${internalDeps.join(',')}]`); + internalDeps.forEach(dep => traverse(dep, depth + 1)); + } + }; + traverse(typeName); + return result.join(';'); + }; + + // Process export block and annotate with dependencies hash + for (const nodePath of path.get('body')) { + if ( + t.isExportNamedDeclaration(nodePath.node) && + !nodePath.node.declaration && + nodePath.node.specifiers != null + ) { + const specifiers = nodePath.node.specifiers.map(specifier => { + // $FlowIgnore[incompatible-type] nodePath is refined above + // $FlowIgnore[incompatible-use] + const name: string = specifier.exported.name; + if (declarations.has(name)) { + const hash = generateTypeHash(name); + let comment = ` ${hash}`; + + if (outputDebugAnnotations) { + const deps = getAllDependencies(name); + deps.delete(name); + const depTree = getCompactDependencyTree(name); + comment += + `, Deps: [${Array.from(deps).join(', ')}]` + + `, Total: ${deps.size}` + + (depTree.length ? `, Tree: ${depTree}` : ''); + } + + return t.addComment(specifier, 'trailing', comment, true); + } + return specifier; + }); + // $FlowIgnore[prop-missing] + // $FlowIgnore[incompatible-type] + nodePath.node.specifiers = specifiers; + } + } + }, + }, + }; + + /** + * Collect all direct type references from a TypeScript AST node. + */ + function getTypeReferencesForNode( + node: BabelNode, + refs: Set = new Set(), + ): Set { + if (!node) { + return refs; + } + + // Handle type references + if (t.isTSTypeReference(node) && node.typeName) { + refs.add(extractQualifiedName(node.typeName)); + } + + // Handle interface extends + if (t.isTSInterfaceDeclaration(node) && node.extends) { + for (const extend of node.extends) { + if (extend.expression) { + refs.add(extractQualifiedName(extend.expression)); } } - }, - }, -}; + } + + // Handle class extends and implements + if (t.isClassDeclaration(node)) { + if (node.superClass && node.superClass.type === 'Identifier') { + refs.add(node.superClass.name); + } + + if (node.implements) { + for (const impl of node.implements) { + if (impl.expression) { + refs.add(extractQualifiedName(impl.expression)); + } + } + } + } + + // Handle type parameters + if (node.typeParameters && node.typeParameters.params) { + for (const param of node.typeParameters.params) { + getTypeReferencesForNode(param, refs); + if (param.constraint) { + getTypeReferencesForNode(param.constraint, refs); + } + if (param.default) { + getTypeReferencesForNode(param.default, refs); + } + } + } + + // Handle indexed access types (`T['key']`) + if (t.isTSIndexedAccessType(node)) { + getTypeReferencesForNode(node.objectType, refs); + getTypeReferencesForNode(node.indexType, refs); + } + + // Handle conditional types (`T extends U ? X : Y`) + if (t.isTSConditionalType(node)) { + getTypeReferencesForNode(node.checkType, refs); + getTypeReferencesForNode(node.extendsType, refs); + getTypeReferencesForNode(node.trueType, refs); + getTypeReferencesForNode(node.falseType, refs); + } + + // Handle mapped types (`{ [K in keyof T]: X }`) + if (t.isTSMappedType(node)) { + if (node.typeParameter && node.typeParameter.constraint) { + getTypeReferencesForNode(node.typeParameter.constraint, refs); + } + if (node.typeAnnotation) { + getTypeReferencesForNode(node.typeAnnotation, refs); + } + } + + // Recursively traverse all properties + for (const key in node) { + // $FlowIgnore[invalid-computed-prop] + const value = node[key]; + if (Array.isArray(value)) { + value.forEach(item => getTypeReferencesForNode(item, refs)); + } else if (typeof value === 'object' && value !== null) { + getTypeReferencesForNode(value, refs); + } + } -/** - * Collect all direct type references from a TypeScript AST node. - */ -function getTypeReferencesForNode( - node: BabelNode, - refs: Set = new Set(), -): Set { - if (!node) { return refs; } - // Handle type references - if (t.isTSTypeReference(node) && node.typeName) { - refs.add(extractQualifiedName(node.typeName)); - } - - // Handle interface extends - if (t.isTSInterfaceDeclaration(node) && node.extends) { - for (const extend of node.extends) { - if (extend.expression) { - refs.add(extractQualifiedName(extend.expression)); - } - } - } - - // Handle class extends and implements - if (t.isClassDeclaration(node)) { - if (node.superClass && node.superClass.type === 'Identifier') { - refs.add(node.superClass.name); + function extractQualifiedName( + node: BabelNodeIdentifier | BabelNodeTSQualifiedName, + ): string { + if (t.isIdentifier(node)) { + return node.name; } - if (node.implements) { - for (const impl of node.implements) { - if (impl.expression) { - refs.add(extractQualifiedName(impl.expression)); + if (t.isTSQualifiedName(node)) { + let fullName = ''; + let current = node; + + while (t.isTSQualifiedName(current)) { + if (current.right && current.right.name) { + fullName = '.' + current.right.name + fullName; } + // $FlowIgnore[prop-missing] + // $FlowIgnore[incompatible-type] + current = current.left; + } + + if (t.isIdentifier(current) && current.name) { + return current.name + fullName; } } - } - // Handle type parameters - if (node.typeParameters && node.typeParameters.params) { - for (const param of node.typeParameters.params) { - getTypeReferencesForNode(param, refs); - if (param.constraint) { - getTypeReferencesForNode(param.constraint, refs); - } - if (param.default) { - getTypeReferencesForNode(param.default, refs); - } - } + throw new Error( + `Failed to parse type name from node of type: ${node.type}. Expected Identifier or TSQualifiedName.`, + ); } - - // Handle indexed access types (`T['key']`) - if (t.isTSIndexedAccessType(node)) { - getTypeReferencesForNode(node.objectType, refs); - getTypeReferencesForNode(node.indexType, refs); - } - - // Handle conditional types (`T extends U ? X : Y`) - if (t.isTSConditionalType(node)) { - getTypeReferencesForNode(node.checkType, refs); - getTypeReferencesForNode(node.extendsType, refs); - getTypeReferencesForNode(node.trueType, refs); - getTypeReferencesForNode(node.falseType, refs); - } - - // Handle mapped types (`{ [K in keyof T]: X }`) - if (t.isTSMappedType(node)) { - if (node.typeParameter && node.typeParameter.constraint) { - getTypeReferencesForNode(node.typeParameter.constraint, refs); - } - if (node.typeAnnotation) { - getTypeReferencesForNode(node.typeAnnotation, refs); - } - } - - // Recursively traverse all properties - for (const key in node) { - // $FlowIgnore[invalid-computed-prop] - const value = node[key]; - if (Array.isArray(value)) { - value.forEach(item => getTypeReferencesForNode(item, refs)); - } else if (typeof value === 'object' && value !== null) { - getTypeReferencesForNode(value, refs); - } - } - - return refs; } -function extractQualifiedName( - node: BabelNodeIdentifier | BabelNodeTSQualifiedName, -): string { - if (t.isIdentifier(node)) { - return node.name; - } - - if (t.isTSQualifiedName(node)) { - let fullName = ''; - let current = node; - - while (t.isTSQualifiedName(current)) { - if (current.right && current.right.name) { - fullName = '.' + current.right.name + fullName; - } - // $FlowIgnore[prop-missing] - // $FlowIgnore[incompatible-type] - current = current.left; - } - - if (t.isIdentifier(current) && current.name) { - return current.name + fullName; - } - } - - throw new Error( - `Failed to parse type name from node of type: ${node.type}. Expected Identifier or TSQualifiedName.`, - ); -} - -module.exports = visitor; +module.exports = createVersionExportedApis;