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
This commit is contained in:
Nicola Corti
2026-09-15 11:23:27 -07:00
committed by meta-codesync[bot]
parent ec9952a62d
commit 07e4bf34d0
4 changed files with 179 additions and 4 deletions
+5 -4
View File
@@ -14,16 +14,16 @@
"cxx-api-validate": "python -m scripts.cxx-api.parser --validate", "cxx-api-validate": "python -m scripts.cxx-api.parser --validate",
"flow-check": "flow full-check", "flow-check": "flow full-check",
"flow": "flow", "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-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-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-cpp": "node ./scripts/clang-format.js",
"format-javascript": "prettier --write \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", "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", "featureflags": "yarn --cwd packages/react-native featureflags",
"js-api-diff": "node ./scripts/js-api/diff-api-snapshot", "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-markdown": "markdownlint-cli2 2>&1",
"lint": "eslint --max-warnings 0 .", "lint": "eslint --max-warnings 0 .",
"preinstall": "node ./scripts/try-set-hermes-compiler-prebuilt.js", "preinstall": "node ./scripts/try-set-hermes-compiler-prebuilt.js",
@@ -101,6 +101,7 @@
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"jest-snapshot": "^29.7.0", "jest-snapshot": "^29.7.0",
"jsonc-parser": "2.2.1", "jsonc-parser": "2.2.1",
"ktfmt": "0.59.0",
"markdownlint-cli2": "^0.17.2", "markdownlint-cli2": "^0.17.2",
"markdownlint-rule-relative-links": "^3.0.0", "markdownlint-rule-relative-links": "^3.0.0",
"memfs": "^4.38.2", "memfs": "^4.38.2",
+81
View File
@@ -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();
+88
View File
@@ -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,
};
+5
View File
@@ -6373,6 +6373,11 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== 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: language-subtag-registry@^0.3.20:
version "0.3.23" version "0.3.23"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7"