From 07e4bf34d0521cf1a0cd94a77c75ff7d3b76145d Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Tue, 15 Sep 2026 11:23:27 -0700 Subject: [PATCH] Add Kotlin to `yarn format` (#58462) Summary: Pull Request resolved: https://github.com/react/react-native/pull/58462 Add `yarn format-kotlin` and `yarn format-check-kotlin` using the npm `ktfmt` package and its bundled formatter jar. This adds the offline package mirror and workspace lock entry without changing existing Kotlin source formatting or Gradle configuration. The wrapper discovers a suitable JDK when available and otherwise prints environment-specific Java 17 setup guidance before skipping Kotlin. allow-large-files: The npm package intentionally contains the upstream ktfmt executable jar so offline and public installs use the same formatter. Changelog: [Internal] Reviewed By: javache Differential Revision: D119487612 fbshipit-source-id: cb699a1286cffbf461ef69ed66f694e3f69415d0 --- package.json | 9 ++-- scripts/format-kotlin.js | 81 ++++++++++++++++++++++++++++++++++++ scripts/format-utils.js | 88 ++++++++++++++++++++++++++++++++++++++++ yarn.lock | 5 +++ 4 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 scripts/format-kotlin.js create mode 100644 scripts/format-utils.js diff --git a/package.json b/package.json index 55a1e784234..d98b49bbff4 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,16 @@ "cxx-api-validate": "python -m scripts.cxx-api.parser --validate", "flow-check": "flow full-check", "flow": "flow", - "format-check": "yarn format-check-javascript && yarn format-check-cpp", + "format-check": "yarn format-check-javascript && yarn format-check-cpp && yarn format-check-kotlin", "format-check-cpp": "node ./scripts/clang-format.js --check", "format-check-javascript": "prettier --check \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", - "format": "yarn format-javascript && yarn format-cpp", + "format-check-kotlin": "node ./scripts/format-kotlin.js --check", + "format": "yarn format-javascript && yarn format-cpp && yarn format-kotlin", "format-cpp": "node ./scripts/clang-format.js", "format-javascript": "prettier --write \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", + "format-kotlin": "node ./scripts/format-kotlin.js", "featureflags": "yarn --cwd packages/react-native featureflags", "js-api-diff": "node ./scripts/js-api/diff-api-snapshot", - "lint-kotlin-check": "./gradlew ktfmtCheck", - "lint-kotlin": "./gradlew ktfmtFormat", "lint-markdown": "markdownlint-cli2 2>&1", "lint": "eslint --max-warnings 0 .", "preinstall": "node ./scripts/try-set-hermes-compiler-prebuilt.js", @@ -101,6 +101,7 @@ "jest-junit": "^16.0.0", "jest-snapshot": "^29.7.0", "jsonc-parser": "2.2.1", + "ktfmt": "0.59.0", "markdownlint-cli2": "^0.17.2", "markdownlint-rule-relative-links": "^3.0.0", "memfs": "^4.38.2", diff --git a/scripts/format-kotlin.js b/scripts/format-kotlin.js new file mode 100644 index 00000000000..9879f9bcbf9 --- /dev/null +++ b/scripts/format-kotlin.js @@ -0,0 +1,81 @@ +/** + * 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. + * + * @noflow + * @format + */ + +'use strict'; + +const {findJava, warnMissingJava} = require('./format-utils'); +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const {globSync} = require('tinyglobby'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const KTFMT_JAR = require.resolve('ktfmt/lib/ktfmt.jar'); +const GENERATED_MARKER = Buffer.from('@' + 'generated'); +const MINIMUM_JAVA_VERSION = 17; +const MAX_FILES_PER_PROCESS = 100; +const MAX_HEADER_BYTES = 4096; +const IGNORE = [ + '**/build/**', + '**/com/facebook/yoga/**', + '**/hermes-engine/**', + '**/internal/featureflags/**', + '**/node_modules/**', + '**/systeminfo/ReactNativeVersion.kt', +]; + +function isGenerated(file) { + const fd = fs.openSync(path.resolve(REPO_ROOT, file), 'r'); + try { + const header = Buffer.alloc(MAX_HEADER_BYTES); + const bytesRead = fs.readSync(fd, header, 0, header.length, 0); + return header.subarray(0, bytesRead).includes(GENERATED_MARKER); + } finally { + fs.closeSync(fd); + } +} + +function main() { + const check = process.argv[2] === '--check'; + const java = findJava(MINIMUM_JAVA_VERSION); + if (java == null) { + warnMissingJava('Kotlin'); + return; + } + const files = globSync('**/*.{kt,kts}', { + cwd: REPO_ROOT, + ignore: IGNORE, + }).filter(file => !isGenerated(file)); + for (let i = 0; i < files.length; i += MAX_FILES_PER_PROCESS) { + const result = spawnSync( + java, + [ + '-jar', + KTFMT_JAR, + '--do-not-remove-unused-imports', + ...(check ? ['--dry-run', '--set-exit-if-changed'] : []), + ...files.slice(i, i + MAX_FILES_PER_PROCESS), + ], + {cwd: REPO_ROOT, stdio: 'inherit'}, + ); + if (result.error != null) { + throw result.error; + } + if (result.signal != null) { + process.kill(process.pid, result.signal); + return; + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + } +} + +main(); diff --git a/scripts/format-utils.js b/scripts/format-utils.js new file mode 100644 index 00000000000..7b4dac220cb --- /dev/null +++ b/scripts/format-utils.js @@ -0,0 +1,88 @@ +/** + * 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. + * + * @noflow + * @format + */ + +'use strict'; + +const {spawnSync} = require('node:child_process'); +const path = require('node:path'); + +const REPO_ROOT = path.resolve(__dirname, '..'); + +let metaUtils = null; +try { + metaUtils = require('./format-utils.fb'); +} catch (error) { + if ( + error == null || + typeof error !== 'object' || + error.code !== 'MODULE_NOT_FOUND' || + !String(error.message).includes("'./format-utils.fb'") + ) { + throw error; + } +} + +const IS_META_CHECKOUT = metaUtils != null; + +function commandVersion(command, prefixArguments = []) { + const result = spawnSync(command, [...prefixArguments, '--version'], { + encoding: 'utf8', + env: {...process.env, PWD: REPO_ROOT}, + }); + return { + output: `${result.stdout ?? ''}\n${result.stderr ?? ''}`, + status: result.status, + }; +} + +function findMetaTool(...relativePath) { + return metaUtils?.findMetaTool(...relativePath) ?? null; +} + +function javaMajorVersion(command) { + const result = spawnSync(command, ['-version'], {encoding: 'utf8'}); + const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`; + const version = /version "(?:1\.)?(\d+)/.exec(output); + return result.status === 0 && version != null ? Number(version[1]) : null; +} + +function findJava(minimumVersion) { + if (process.env.JAVA != null && process.env.JAVA !== '') { + return javaMajorVersion(process.env.JAVA) >= minimumVersion + ? process.env.JAVA + : null; + } + + const candidates = []; + candidates.push(...(metaUtils?.findJavaCandidates() ?? [])); + candidates.push('java'); + + return ( + candidates.find(command => javaMajorVersion(command) >= minimumVersion) ?? + null + ); +} + +function warnMissingJava(language) { + const instructions = + metaUtils?.missingJavaInstructions() ?? + 'Please install a JDK of your choice with Java 17 or newer and make sure the `java` command is in your PATH, or set JAVA=/path/to/java.'; + console.warn( + `warning: Skipping ${language} formatting because Java 17 or newer was not found.\n${instructions}`, + ); +} + +module.exports = { + commandVersion, + findJava, + findMetaTool, + IS_META_CHECKOUT, + warnMissingJava, +}; diff --git a/yarn.lock b/yarn.lock index 5126518dbbe..3327c4cc333 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6373,6 +6373,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +ktfmt@0.59.0: + version "0.59.0" + resolved "https://registry.yarnpkg.com/ktfmt/-/ktfmt-0.59.0.tgz#99f98b81dbdc7f1487dfbc9850eb17b3780cf6d5" + integrity sha512-lOEn/7y2Ez2/nxDTn5EwJv6BSugB8BtzY2Gn6GvyLIAjdUf3xgKzirIxD57t/vu5I6eybivmVtONI3WGXyZ3lw== + language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7"