From a61c32b6a342ec543ec1817b79255fdb8917a372 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 11 Sep 2026 05:35:23 -0700 Subject: [PATCH 1/5] Match Prettier coverage with `arc f` Summary: Use the public Prettier configuration for the complete OSS JavaScript and documentation surface. Expose `yarn format-javascript` and `yarn format-check-javascript`, and compose them into the repository-wide commands. Changelog: [Internal] Differential Revision: D119487610 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index cdba8ff29db9..b5dc3eafc1ef 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ "cxx-api-validate": "python -m scripts.cxx-api.parser --validate", "flow-check": "flow full-check", "flow": "flow", - "format-check": "prettier --list-different \"./**/*.{js,md,yml,ts,tsx}\"", - "format": "npm run prettier && npm run clang-format", + "format-check": "yarn format-check-javascript", + "format-check-javascript": "prettier --check \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", + "format": "yarn format-javascript && yarn clang-format", + "format-javascript": "prettier --write \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", "featureflags": "yarn --cwd packages/react-native featureflags", "js-api-diff": "node ./scripts/js-api/diff-api-snapshot", "lint-kotlin-check": "./gradlew ktfmtCheck", @@ -24,7 +26,6 @@ "lint-markdown": "markdownlint-cli2 2>&1", "lint": "eslint --max-warnings 0 .", "preinstall": "node ./scripts/try-set-hermes-compiler-prebuilt.js", - "prettier": "prettier --write \"./**/*.{js,md,yml,ts,tsx}\"", "shellcheck": "./.github/workflow-scripts/analyze_scripts.sh", "start": "yarn --cwd packages/rn-tester start", "set-version": "node ./scripts/releases/set-version.js", From 636b709191ae53df900714e27acfb133a981e17c Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 11 Sep 2026 05:35:23 -0700 Subject: [PATCH 2/5] Check all C-family sources with clang-format Summary: Add npm-driven clang-format commands for exported C-family sources. Expose `yarn format-cpp` and `yarn format-check-cpp`, and compose them into the repository-wide commands. The wrapper automatically selects the repository-provided formatter for its environment. Changelog: [Internal] Differential Revision: D119487611 --- .clang-format-ignore | 3 ++ package.json | 7 +++-- scripts/clang-format.js | 68 +++++++++++++++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/.clang-format-ignore b/.clang-format-ignore index c955f0ad4d5d..8d232f8c190b 100644 --- a/.clang-format-ignore +++ b/.clang-format-ignore @@ -1,3 +1,6 @@ +**/Pods/** +**/build/** +**/node_modules/** packages/react-native/React/I18n/FBXXHashUtils.h packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/** packages/react-native/ReactAndroid/src/main/jni/third-party/** diff --git a/package.json b/package.json index b5dc3eafc1ef..55a1e7842346 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,16 @@ "build-android": "./gradlew :packages:react-native:ReactAndroid:build", "build": "node ./scripts/build/build.js", "build-types": "node ./scripts/js-api/build-types", - "clang-format": "node ./scripts/clang-format.js", "clean": "node ./scripts/build/clean.js", "cxx-api-build": "python -m scripts.cxx-api.parser", "cxx-api-validate": "python -m scripts.cxx-api.parser --validate", "flow-check": "flow full-check", "flow": "flow", - "format-check": "yarn format-check-javascript", + "format-check": "yarn format-check-javascript && yarn format-check-cpp", + "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 clang-format", + "format": "yarn format-javascript && yarn format-cpp", + "format-cpp": "node ./scripts/clang-format.js", "format-javascript": "prettier --write \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", "featureflags": "yarn --cwd packages/react-native featureflags", "js-api-diff": "node ./scripts/js-api/diff-api-snapshot", diff --git a/scripts/clang-format.js b/scripts/clang-format.js index abdf8c81df45..9b7181374b19 100644 --- a/scripts/clang-format.js +++ b/scripts/clang-format.js @@ -10,18 +10,49 @@ 'use strict'; -const dotslash = require('fb-dotslash'); 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 CLANG_FORMAT = path.join(__dirname, 'clang-format'); +const OSS_CLANG_FORMAT_DOTSLASH = path.join(__dirname, 'clang-format'); const GENERATED_MARKER = Buffer.from('@' + 'generated'); +const IGNORE_FILE = path.join(REPO_ROOT, '.clang-format-ignore'); const MAX_HEADER_BYTES = 4096; const MAX_FILES_PER_PROCESS = 30; +const SOURCE_GLOB = '**/*.{c,cc,cpp,cu,cuh,cxx,h,hh,hpp,hxx,m,mm,proto,tcc}'; + +function findClangFormat() { + if (process.env.CLANG_FORMAT != null && process.env.CLANG_FORMAT !== '') { + return {command: process.env.CLANG_FORMAT, prefixArguments: []}; + } + + try { + const metaClangFormat = require('./clang-format.fb').findMetaClangFormat(); + if (metaClangFormat != null) { + return metaClangFormat; + } + } catch (error) { + if ( + error == null || + error.code !== 'MODULE_NOT_FOUND' || + !String(error.message).includes("'./clang-format.fb'") + ) { + throw error; + } + } + + return { + command: + process.env.DOTSLASH != null && process.env.DOTSLASH !== '' + ? process.env.DOTSLASH + : require('fb-dotslash'), + prefixArguments: [OSS_CLANG_FORMAT_DOTSLASH], + }; +} + /** @param {string} file */ function isGenerated(file) { let fd; @@ -41,16 +72,33 @@ function isGenerated(file) { } function main() { + const arguments_ = process.argv.slice(2); + const check = arguments_.includes('--check'); + const clangFormat = findClangFormat(); + const positionalArguments = arguments_.filter( + argument => argument !== '--check', + ); + const ignore = fs + .readFileSync(IGNORE_FILE, 'utf8') + .split('\n') + .map(line => line.trim()) + .filter(line => line !== '' && !line.startsWith('#')); + const discoveredFiles = globSync(SOURCE_GLOB, {cwd: REPO_ROOT, ignore}); const files = - process.argv.length > 2 - ? process.argv.slice(2) - : globSync('*/**/*.{h,cpp,m,mm}', {cwd: REPO_ROOT}); + positionalArguments.length > 0 + ? positionalArguments.filter(file => discoveredFiles.includes(file)) + : discoveredFiles; const sourceFiles = files.filter(file => !isGenerated(file)); + let exitStatus = 0; for (let i = 0; i < sourceFiles.length; i += MAX_FILES_PER_PROCESS) { + const formatterArguments = [ + ...(check ? ['--dry-run', '--Werror'] : ['-i']), + ...sourceFiles.slice(i, i + MAX_FILES_PER_PROCESS), + ]; const result = spawnSync( - dotslash, - [CLANG_FORMAT, '-i', ...sourceFiles.slice(i, i + MAX_FILES_PER_PROCESS)], + clangFormat.command, + [...clangFormat.prefixArguments, ...formatterArguments], { cwd: REPO_ROOT, stdio: 'inherit', @@ -61,13 +109,13 @@ function main() { throw result.error; } if (result.signal != null) { - process.kill(process.pid, result.signal); - return; + throw new Error(`clang-format was terminated by ${result.signal}`); } if (result.status !== 0) { - process.exit(result.status ?? 1); + exitStatus = result.status ?? 1; } } + process.exitCode = exitStatus; } main(); From 3961cdd70476087e102d6e5cd8176fd31e7d4d87 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 11 Sep 2026 05:35:23 -0700 Subject: [PATCH 3/5] Add Kotlin to `yarn format` Summary: 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] Differential Revision: D119487612 --- 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 55a1e7842346..d98b49bbff4b 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 000000000000..9879f9bcbf97 --- /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 000000000000..7b4dac220cb3 --- /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 5126518dbbe1..3327c4cc333c 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" From 793a8c59770ef7467cf1fb2db57be17b069ca39e Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 11 Sep 2026 05:35:23 -0700 Subject: [PATCH 4/5] Remove the ktfmt Gradle plugin Summary: Remove the ktfmt Gradle plugin and its task wiring now that Kotlin formatting is provided by the repository npm command. Changelog: [Internal] Differential Revision: D119504761 --- build.gradle.kts | 40 ------------------- packages/gradle-plugin/build.gradle.kts | 23 ----------- .../gradle-plugin/gradle/libs.versions.toml | 2 - .../build.gradle.kts | 1 - .../settings-plugin/build.gradle.kts | 1 - .../shared-testutil/build.gradle.kts | 1 - .../gradle-plugin/shared/build.gradle.kts | 1 - .../ReactAndroid/build.gradle.kts | 1 - .../react-native/gradle/libs.versions.toml | 2 - 9 files changed, 72 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 19fcc1df1a66..e1320dc6d06c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,6 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.binary.compatibility.validator) apply true alias(libs.plugins.android.test) apply false - alias(libs.plugins.ktfmt) apply true } val reactAndroidProperties = java.util.Properties() @@ -176,42 +175,3 @@ if (hermesSubstitution != null) { } } } - -ktfmt { - blockIndent.set(2) - continuationIndent.set(4) - maxWidth.set(100) - removeUnusedImports.set(false) - manageTrailingCommas.set(false) -} - -// Configure ktfmt tasks to include gradle-plugin -listOf("ktfmtCheck", "ktfmtFormat").forEach { taskName -> - tasks.named(taskName) { dependsOn(gradle.includedBuild("gradle-plugin").task(":$taskName")) } -} - -allprojects { - // Apply exclusions for specific files that should not be formatted - val excludePatterns = listOf( - "**/build/**", - "**/hermes-engine/**", - "**/internal/featureflags/**", - "**/systeminfo/ReactNativeVersion.kt", - ) - listOf( - com.ncorti.ktfmt.gradle.tasks.KtfmtCheckTask::class, - com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask::class, - ) - .forEach { tasks.withType(it) { exclude(excludePatterns) } } - - // Disable the problematic ktfmt script tasks due to symbolic link issues in subprojects - afterEvaluate { - listOf("ktfmtCheckScripts", "ktfmtFormatScripts").forEach { - tasks.findByName(it)?.enabled = false - } - } -} - -// We intentionally disable the `ktfmtCheck` tasks as the formatting is primarly handled inside -// fbsource -allprojects { tasks.withType() { enabled = false } } diff --git a/packages/gradle-plugin/build.gradle.kts b/packages/gradle-plugin/build.gradle.kts index 80021f061fb7..48ecd29695e8 100644 --- a/packages/gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/build.gradle.kts @@ -7,7 +7,6 @@ plugins { alias(libs.plugins.kotlin.jvm).apply(false) - alias(libs.plugins.ktfmt).apply(true) } tasks.register("build") { @@ -27,25 +26,3 @@ tasks.register("clean") { ":shared:clean", ) } - -tasks.named("ktfmtCheck") { - dependsOn( - ":react-native-gradle-plugin:ktfmtCheck", - ":settings-plugin:ktfmtCheck", - ":shared-testutil:ktfmtCheck", - ":shared:ktfmtCheck", - ) -} - -tasks.named("ktfmtFormat") { - dependsOn( - ":react-native-gradle-plugin:ktfmtFormat", - ":settings-plugin:ktfmtFormat", - ":shared-testutil:ktfmtFormat", - ":shared:ktfmtFormat", - ) -} - -// We intentionally disable the `ktfmtCheck` tasks as the formatting is primarly handled inside -// fbsource -allprojects { tasks.withType() { enabled = false } } diff --git a/packages/gradle-plugin/gradle/libs.versions.toml b/packages/gradle-plugin/gradle/libs.versions.toml index 65dd0b10f53f..8446ff11b096 100644 --- a/packages/gradle-plugin/gradle/libs.versions.toml +++ b/packages/gradle-plugin/gradle/libs.versions.toml @@ -6,7 +6,6 @@ javapoet = "1.13.0" junit = "4.13.2" kotlin = "2.2.0" assertj = "3.25.1" -ktfmt = "0.22.0" [libraries] kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -19,4 +18,3 @@ assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } diff --git a/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts b/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts index 1450a701a71a..1f083f522185 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts +++ b/packages/gradle-plugin/react-native-gradle-plugin/build.gradle.kts @@ -12,7 +12,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.ktfmt) id("java-gradle-plugin") } diff --git a/packages/gradle-plugin/settings-plugin/build.gradle.kts b/packages/gradle-plugin/settings-plugin/build.gradle.kts index 39a1e490d4aa..15b8ed13087f 100644 --- a/packages/gradle-plugin/settings-plugin/build.gradle.kts +++ b/packages/gradle-plugin/settings-plugin/build.gradle.kts @@ -12,7 +12,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.ktfmt) id("java-gradle-plugin") } diff --git a/packages/gradle-plugin/shared-testutil/build.gradle.kts b/packages/gradle-plugin/shared-testutil/build.gradle.kts index 34591e06e8c5..658759c3ea17 100644 --- a/packages/gradle-plugin/shared-testutil/build.gradle.kts +++ b/packages/gradle-plugin/shared-testutil/build.gradle.kts @@ -12,7 +12,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.ktfmt) } repositories { mavenCentral() } diff --git a/packages/gradle-plugin/shared/build.gradle.kts b/packages/gradle-plugin/shared/build.gradle.kts index 0f62f3310afc..492f47325556 100644 --- a/packages/gradle-plugin/shared/build.gradle.kts +++ b/packages/gradle-plugin/shared/build.gradle.kts @@ -12,7 +12,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.ktfmt) } repositories { mavenCentral() } diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 8a27e3e4f470..39e5e642a5e7 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -18,7 +18,6 @@ plugins { id("com.facebook.react") alias(libs.plugins.android.library) alias(libs.plugins.download) - alias(libs.plugins.ktfmt) } version = project.findProperty("VERSION_NAME")?.toString()!! diff --git a/packages/react-native/gradle/libs.versions.toml b/packages/react-native/gradle/libs.versions.toml index 0bba9087d8dc..0c84940db453 100644 --- a/packages/react-native/gradle/libs.versions.toml +++ b/packages/react-native/gradle/libs.versions.toml @@ -31,7 +31,6 @@ jsc-android = "2026004.0.1" jsr305 = "3.0.2" junit = "4.13.2" kotlin = "2.2.0" -ktfmt = "0.22.0" mockito = "3.12.4" mockito-kotlin = "3.2.0" nexus-publish = "2.0.0" @@ -97,7 +96,6 @@ thoughtworks = {module = "com.thoughtworks.xstream:xstream", version.ref = "xstr android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } download = { id = "de.undercouch.download", version.ref = "download" } -ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus-publish" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } From 554ee9c6b219523d87cf99d311111b7eeab32ae0 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Fri, 11 Sep 2026 05:42:15 -0700 Subject: [PATCH 5/5] Add Java to `yarn format` (#58466) Summary: Pull Request resolved: https://github.com/react/react-native/pull/58466 Add `yarn format-java` and `yarn format-check-java` using the npm `google-java-format` package, which bundles google-java-format 1.23.0. This removes the Gradle build dependency and adds the offline package mirror and workspace lock entries. The wrapper discovers a suitable JDK when available and otherwise prints environment-specific Java 17 setup guidance before skipping Java. Changelog: [Internal] Reviewed By: javache Differential Revision: D119487613 --- package.json | 7 ++- scripts/format-java.js | 106 +++++++++++++++++++++++++++++++++++++++++ yarn.lock | 49 +++++++++++++++++++ 3 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 scripts/format-java.js diff --git a/package.json b/package.json index d98b49bbff4b..9880d40688a4 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,14 @@ "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 && yarn format-check-kotlin", + "format-check": "yarn format-check-javascript && yarn format-check-cpp && yarn format-check-kotlin && yarn format-check-java", "format-check-cpp": "node ./scripts/clang-format.js --check", + "format-check-java": "node ./scripts/format-java.js --check", "format-check-javascript": "prettier --check \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", "format-check-kotlin": "node ./scripts/format-kotlin.js --check", - "format": "yarn format-javascript && yarn format-cpp && yarn format-kotlin", + "format": "yarn format-javascript && yarn format-cpp && yarn format-kotlin && yarn format-java", "format-cpp": "node ./scripts/clang-format.js", + "format-java": "node ./scripts/format-java.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", @@ -93,6 +95,7 @@ "flow-eslint": "0.331.0", "flow-parser": "0.331.0", "flow-transform": "0.331.0", + "google-java-format": "1.4.0", "ini": "^5.0.0", "inquirer": "^7.1.0", "jest": "^29.7.0", diff --git a/scripts/format-java.js b/scripts/format-java.js new file mode 100644 index 000000000000..d991b6b38922 --- /dev/null +++ b/scripts/format-java.js @@ -0,0 +1,106 @@ +/** + * 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 GENERATED_MARKER = Buffer.from('@' + 'generated'); +const MINIMUM_JAVA_VERSION = 17; +const MAX_FILES_PER_PROCESS = 30; +const MAX_HEADER_BYTES = 4096; +const IGNORE = [ + '**/Pods/**', + '**/build/**', + '**/com/facebook/yoga/**', + '**/node_modules/**', +]; + +function isGenerated(file) { + let fd; + try { + fd = fs.openSync(path.resolve(REPO_ROOT, file), 'r'); + 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); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Unable to inspect ${file}: ${message}`, {cause: error}); + } finally { + if (fd != null) { + fs.closeSync(fd); + } + } +} + +function findGoogleJavaFormatJar() { + const packageRoot = path.dirname( + require.resolve('google-java-format/package.json'), + ); + const jars = fs + .readdirSync(path.join(packageRoot, 'lib')) + .filter(file => file.endsWith('-all-deps.jar')); + if (jars.length !== 1) { + throw new Error( + `Expected one google-java-format jar, found ${jars.length}.`, + ); + } + return path.join(packageRoot, 'lib', jars[0]); +} + +function main() { + const check = process.argv[2] === '--check'; + const java = findJava(MINIMUM_JAVA_VERSION); + if (java == null) { + warnMissingJava('Java'); + return; + } + const googleJavaFormatJar = findGoogleJavaFormatJar(); + const files = globSync('**/*.java', {cwd: REPO_ROOT, ignore: IGNORE}).filter( + file => !isGenerated(file), + ); + + let exitStatus = 0; + for (let i = 0; i < files.length; i += MAX_FILES_PER_PROCESS) { + const result = spawnSync( + java, + [ + '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + '-jar', + googleJavaFormatJar, + ...(check ? ['--dry-run', '--set-exit-if-changed'] : ['--replace']), + ...files.slice(i, i + MAX_FILES_PER_PROCESS), + ], + {cwd: REPO_ROOT, stdio: 'inherit'}, + ); + if (result.error != null) { + throw result.error; + } + if (result.signal != null) { + throw new Error(`google-java-format was terminated by ${result.signal}`); + } + if (result.status !== 0) { + exitStatus = result.status ?? 1; + } + } + process.exitCode = exitStatus; +} + +main(); diff --git a/yarn.lock b/yarn.lock index 3327c4cc333c..ff0546f73995 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2912,6 +2912,11 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + author-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/author-regex/-/author-regex-1.0.0.tgz#d08885be6b9bbf9439fe087c76287245f0a81450" @@ -3146,6 +3151,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.4.tgz#589dab11c0018d0366be64cd8bf12c8dbecc8326" + integrity sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg== + dependencies: + balanced-match "^1.0.0" + brace-expansion@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" @@ -5041,6 +5053,17 @@ glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -5085,6 +5108,15 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +google-java-format@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/google-java-format/-/google-java-format-1.4.0.tgz#d944b7a20f0a3729318b5c120931178843ed9a82" + integrity sha512-TlO1nUogUW6PonVL4xZCciVoJcjB2Nxo/dEY5M8GC5TJnQi6A4XeqJoiOot/To20350wPup9aQfP3Ia6GR+vEg== + dependencies: + async "^3.2.4" + glob "^8.1.0" + resolve "^1.22.8" + gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -7220,6 +7252,13 @@ minimatch@^10.0.1, minimatch@^10.2.2: dependencies: brace-expansion "^5.0.5" +minimatch@^5.0.1: + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== + dependencies: + brace-expansion "^2.0.1" + minimatch@^9.0.4: version "9.0.9" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" @@ -8080,6 +8119,16 @@ resolve@^1.1.6, resolve@^1.14.2, resolve@^1.20.0, resolve@~1.22.1, resolve@~1.22 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.8: + version "1.22.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.5: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c"