From b54efb8d0dc1fe5fbbf6c20df8e2a717bd8beb70 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Thu, 6 Feb 2025 08:49:29 -0800 Subject: [PATCH] Update multi-platform handling in build-types, add debug logs (#49224) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49224 Refactor / quality pass. - Remove `micromatch`, replace with glob ignores. - Move and simplify platfom-specific file logic: mutate `files` as a single `Set`, reduce iterations. - This is reconfigured so that the input file path globs need only match `*.js` sources. - Introduce `debug` logs and expose convenience `--debug` script flag. - Move output error detection into inner function implementation. Changelog: [Internal] Metro changelog: Internal Reviewed By: j-piasecki Differential Revision: D69240543 fbshipit-source-id: c2faef8212a2995936362b3d33d189c405bd879d --- flow-typed/npm/debug_v2.x.x.js | 6 +- package.json | 1 + scripts/build/build-types.js | 144 +++++++++--------- .../build/build-types/translateSourceFile.js | 11 +- 4 files changed, 88 insertions(+), 74 deletions(-) diff --git a/flow-typed/npm/debug_v2.x.x.js b/flow-typed/npm/debug_v2.x.x.js index 0a42108058d..c104d8aaece 100644 --- a/flow-typed/npm/debug_v2.x.x.js +++ b/flow-typed/npm/debug_v2.x.x.js @@ -12,5 +12,9 @@ // https://www.npmjs.com/package/debug declare module 'debug' { - declare module.exports: (namespace: string) => (...Array) => void; + declare module.exports: { + (namespace: string): (...Array) => void, + enable(match: string): void, + disable(): void, + }; } diff --git a/package.json b/package.json index 2dc5fcb7502..245546e1699 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "chalk": "^4.0.0", "clang-format": "^1.8.0", "connect": "^3.6.5", + "debug": "^2.2.0", "deep-equal": "1.1.1", "eslint": "^8.57.0", "eslint-config-prettier": "^8.5.0", diff --git a/scripts/build/build-types.js b/scripts/build/build-types.js index 6e53f452192..45140cf7fbf 100644 --- a/scripts/build/build-types.js +++ b/scripts/build/build-types.js @@ -14,40 +14,46 @@ require('../babel-register').registerForScript(); const {PACKAGES_DIR, REPO_ROOT} = require('../consts'); const translateSourceFile = require('./build-types/translateSourceFile'); const chalk = require('chalk'); -const {promises: fs} = require('fs'); +const debugModule = require('debug'); +const debug = require('debug')('build-types'); +const {existsSync, promises: fs} = require('fs'); const glob = require('glob'); -const micromatch = require('micromatch'); const path = require('path'); const {parseArgs} = require('util'); -const TYPES_DIR = 'types_generated'; -const IGNORE_PATTERN = '**/__{tests,mocks,fixtures,flowtests}__/**'; +const OUTPUT_DIR = 'types_generated'; + +const IGNORE_PATTERNS = [ + '**/__{tests,mocks,fixtures,flowtests}__/**', + '**/*.{macos,windows}.js', +]; const SOURCE_PATTERNS = [ - 'react-native/Libraries/Alert/**/*.{js,flow}', - 'react-native/Libraries/ActionSheetIOS/**/*.{js,flow}', - 'react-native/Libraries/Components/ToastAndroid/*.{js,flow}', + 'react-native/Libraries/ActionSheetIOS/**/*.js', + 'react-native/Libraries/Alert/**/*.js', + 'react-native/Libraries/Components/ToastAndroid/*.js', + 'react-native/Libraries/ReactNative/RootTag.js', + 'react-native/Libraries/Settings/**/*.js', 'react-native/Libraries/TurboModule/RCTExport.js', 'react-native/Libraries/Types/RootTagTypes.js', - 'react-native/Libraries/ReactNative/RootTag.js', 'react-native/Libraries/Utilities/Platform.js', - 'react-native/Libraries/Settings/**/*.js', - 'react-native/src/private/specs_DEPRECATED/modules/NativeAlertManager.js', 'react-native/src/private/specs_DEPRECATED/modules/NativeActionSheetManager.js', - 'react-native/src/private/specs/modules/NativeToastAndroid.js', + 'react-native/src/private/specs_DEPRECATED/modules/NativeAlertManager.js', 'react-native/src/private/specs_DEPRECATED/modules/NativeSettingsManager.js', + 'react-native/src/private/specs/modules/NativeToastAndroid.js', // TODO(T210505412): Include input packages, e.g. virtualized-lists ]; const config = { options: { + debug: {type: 'boolean'}, help: {type: 'boolean'}, }, }; async function main() { const { - values: {help}, + values: {debug: debugEnabled, help}, } = parseArgs(config); if (help) { @@ -60,13 +66,9 @@ async function main() { return; } - const files = ignoreShadowedFiles( - SOURCE_PATTERNS.flatMap(srcPath => - glob.sync(path.join(PACKAGES_DIR, srcPath), { - nodir: true, - }), - ), - ); + if (debugEnabled) { + debugModule.enable('build-types'); + } console.log( '\n' + @@ -76,32 +78,66 @@ async function main() { '\n', ); - await Promise.all( - files.map(async file => { - if (micromatch.isMatch(file, IGNORE_PATTERN)) { - return; + const files /*: Set */ = new Set( + SOURCE_PATTERNS.flatMap(srcPath => + glob.sync(path.join(PACKAGES_DIR, srcPath), { + nodir: true, + ignore: IGNORE_PATTERNS, + }), + ), + ); + + // Require common interface file (js.flow) or base implementation (.js) for + // platform-specific files (.android.js or .ios.js) + for (const file of files) { + const [pathWithoutExt, extension] = splitPathAndExtension(file); + + if (/(\.android\.js|\.ios\.js)$/.test(extension)) { + files.delete(file); + + let resolved = false; + + for (const ext of ['.js.flow', '.js']) { + let interfaceFile = pathWithoutExt + ext; + + if (files.has(interfaceFile)) { + resolved = true; + break; + } + + if (existsSync(interfaceFile)) { + files.add(interfaceFile); + resolved = true; + debug( + 'Resolved %s to %s', + path.relative(REPO_ROOT, file), + path.relative(REPO_ROOT, interfaceFile), + ); + break; + } } + if (!resolved) { + throw new Error( + `No common interface found for ${file}.[android|ios].js. This ` + + 'should either be a base .js implementation or a .js.flow interface file.', + ); + } + } + } + + await Promise.all( + Array.from(files).map(async file => { const buildPath = getBuildPath(file); const source = await fs.readFile(file, 'utf-8'); - await fs.mkdir(path.dirname(buildPath), {recursive: true}); try { const typescriptDef = await translateSourceFile(source); - if ( - /Unsupported feature: Translating ".*" is currently not supported/.test( - typescriptDef, - ) - ) { - throw new Error( - 'Syntax unsupported by flow-api-translator used in ' + file, - ); - } - + await fs.mkdir(path.dirname(buildPath), {recursive: true}); await fs.writeFile(buildPath, typescriptDef); } catch (e) { - console.error(`Failed to build ${path.relative(REPO_ROOT, file)}`); + console.error(`Failed to build ${path.relative(REPO_ROOT, file)}\n`, e); } }), ); @@ -117,7 +153,7 @@ function getBuildPath(file /*: string */) /*: string */ { return path.join( packageDir, file - .replace(packageDir, TYPES_DIR) + .replace(packageDir, OUTPUT_DIR) .replace(/\.js\.flow$/, '.js') .replace(/\.js$/, '.d.ts'), ); @@ -132,42 +168,6 @@ function splitPathAndExtension(file /*: string */) /*: [string, string] */ { ]; } -function ignoreShadowedFiles(files /*: Array */) /*: Array */ { - const commonInterfaceFiles /*: Set */ = new Set(); - const result /*: Array */ = []; - - // Find all common interface files - for (const file of files) { - const [pathWithoutExt, extension] = splitPathAndExtension(file); - if (/(\.js|\.flow)$/.test(extension)) { - commonInterfaceFiles.add(pathWithoutExt); - } - } - - for (const file of files) { - const [pathWithoutExt, extension] = splitPathAndExtension(file); - - // Skip android and ios files from being generated and enforce that they - // have a common interface file, either in the form of .js.flow or .js file - if (/(\.android\.js|\.ios\.js)$/.test(extension)) { - if (!commonInterfaceFiles.has(pathWithoutExt)) { - throw new Error(`No common interface found for ${file}`); - } - continue; - } - - // Skip desktop files and don't enforce common interface for them as they - // are entirely ignored by the current flow config - if (/(\.windows\.js|\.macos\.js)$/.test(extension)) { - continue; - } - - result.push(file); - } - - return result; -} - if (require.main === module) { // eslint-disable-next-line no-void void main(); diff --git a/scripts/build/build-types/translateSourceFile.js b/scripts/build/build-types/translateSourceFile.js index 10b6ec984e3..a7ad44ec2c3 100644 --- a/scripts/build/build-types/translateSourceFile.js +++ b/scripts/build/build-types/translateSourceFile.js @@ -21,6 +21,8 @@ const preTransforms: Array = [ require('./transforms/stripPrivateProperties'), ]; const prettierOptions = {parser: 'babel'}; +const unsupportedFeatureRegex = + /Unsupported feature: Translating ".*" is currently not supported/; /** * Translate the public API of a Flow source file to TypeScript definition. @@ -36,10 +38,17 @@ async function translateSourceFile(source: string): Promise { const preTransformResult = await applyTransforms(parsed, preTransforms); // Translate to TypeScript defs - return await translate.translateFlowToTSDef( + const result = await translate.translateFlowToTSDef( preTransformResult.code, prettierOptions, ); + + const unsupportedFeatureMatch = result.match(unsupportedFeatureRegex); + if (unsupportedFeatureMatch != null) { + throw new Error(`Error: ${unsupportedFeatureMatch[0]}`); + } + + return result; } async function applyTransforms(