diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 48b960403f..596173299a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -70,7 +70,7 @@ module.exports = { }, // Node { - files: ['**/.*rc.(c){j,t}s', '**/*.config.?(c){j,t}s'], + files: ['**/.*rc.(c){j,t}s', '**/*.config.?(c){j,t}s', 'scripts/**/*.js'], env: { node: true, }, diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d02ecb0ac0..2bb9921877 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,15 +3,19 @@ - [ ] I have searched [existing issues](https://github.com/facebook/yoga/issues) and this is not a duplicate # Issues and Steps to Reproduce -***Replaces this with steps to repro your issue.*** + +_**Replaces this with steps to repro your issue.**_ # Expected Behavior -***Describe what you expected would happen.*** + +_**Describe what you expected would happen.**_ # Actual Behavior -***Describe what actually happened.*** + +_**Describe what actually happened.**_ # Link to Code -***If you have some code that maintainers can clone/test for themselves, bugs can be resolved much faster. Please paste a link here.*** -***When applicable, use this [fiddle](https://jsfiddle.net/emilsjolander/jckmwztt/) to post a web repro.*** +_**If you have some code that maintainers can clone/test for themselves, bugs can be resolved much faster. Please paste a link here.**_ + +_**When applicable, use this [fiddle](https://jsfiddle.net/emilsjolander/jckmwztt/) to post a web repro.**_ diff --git a/.github/actions/black/action.yml b/.github/actions/black/action.yml index 63b6c423f9..717eb68d8a 100644 --- a/.github/actions/black/action.yml +++ b/.github/actions/black/action.yml @@ -9,7 +9,7 @@ inputs: default: 22.3.0 runs: - using: "composite" + using: 'composite' steps: - name: Ensure supported Python selected uses: actions/setup-python@v4 diff --git a/.github/actions/cache-emsdk/action.yml b/.github/actions/cache-emsdk/action.yml index 4c0553a1e4..da91011876 100644 --- a/.github/actions/cache-emsdk/action.yml +++ b/.github/actions/cache-emsdk/action.yml @@ -1,7 +1,7 @@ name: Cache the installed copy of emsdk and its build artifacts runs: - using: "composite" + using: 'composite' steps: - name: Cache emsdk uses: actions/cache@v3 diff --git a/.github/actions/install-ninja/action.yml b/.github/actions/install-ninja/action.yml index 897a1e7784..704ca7c7cf 100644 --- a/.github/actions/install-ninja/action.yml +++ b/.github/actions/install-ninja/action.yml @@ -1,7 +1,7 @@ name: Install Ninja runs: - using: "composite" + using: 'composite' steps: - name: Install ninja (Linux) if: ${{ runner.os == 'Linux' }} diff --git a/.github/actions/setup-android/action.yml b/.github/actions/setup-android/action.yml index 67ea26b996..fe618c8aa5 100644 --- a/.github/actions/setup-android/action.yml +++ b/.github/actions/setup-android/action.yml @@ -1,7 +1,7 @@ name: Setup Android environment runs: - using: "composite" + using: 'composite' steps: - name: Select Java Version uses: actions/setup-java@v2 diff --git a/.github/actions/setup-apple/action.yml b/.github/actions/setup-apple/action.yml index f8c6203440..52b2316ffb 100644 --- a/.github/actions/setup-apple/action.yml +++ b/.github/actions/setup-apple/action.yml @@ -1,7 +1,7 @@ name: Setup Apple envirionment runs: - using: "composite" + using: 'composite' steps: - uses: maxim-lobanov/setup-xcode@v1 with: diff --git a/.github/actions/setup-cpp/action.yml b/.github/actions/setup-cpp/action.yml index f3aece3074..6057effeed 100644 --- a/.github/actions/setup-cpp/action.yml +++ b/.github/actions/setup-cpp/action.yml @@ -6,7 +6,7 @@ inputs: default: 'Clang' runs: - using: "composite" + using: 'composite' steps: - name: Install Ninja if: ${{ runner.os != 'Windows' }} diff --git a/.github/actions/setup-js/action.yml b/.github/actions/setup-js/action.yml index 341934861a..1e5198cc59 100644 --- a/.github/actions/setup-js/action.yml +++ b/.github/actions/setup-js/action.yml @@ -1,7 +1,7 @@ name: Setup JavaScript envirionment runs: - using: "composite" + using: 'composite' steps: - name: Setup Node environment uses: actions/setup-node@v3 diff --git a/.github/workflows/validate-cpp.yml b/.github/workflows/validate-cpp.yml index 3d7cbe56cf..b3079a31e8 100644 --- a/.github/workflows/validate-cpp.yml +++ b/.github/workflows/validate-cpp.yml @@ -34,7 +34,6 @@ jobs: - name: Unit tests run: ./unit_tests ${{ matrix.mode }} - build_fuzzers: name: Build fuzzers [${{ matrix.toolchain }}][${{ matrix.mode }}] runs-on: ubuntu-latest diff --git a/.github/workflows/validate-format.yml b/.github/workflows/validate-format.yml new file mode 100644 index 0000000000..8c4aaa36e1 --- /dev/null +++ b/.github/workflows/validate-format.yml @@ -0,0 +1,36 @@ +name: Validate Formatting + +on: + pull_request: + push: + branches: + - main + - 'release-*' + workflow_dispatch: + +permissions: + contents: read + +jobs: + format: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup JavaScript + uses: ./.github/actions/setup-js + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Check formatting + run: yarn format-check diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f049d4c531..d38bf51a16 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,21 +14,21 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or +- The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b109361f6..b7809489b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,5 @@ # Contributing to yoga + We want to make contributing to this project as easy and transparent as possible. @@ -7,21 +8,29 @@ possible. Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.fb.com/codeofconduct/) so that you can understand what actions will and will not be tolerated. ## Pull Requests + We actively welcome your pull requests. + 1. Fork the repo and create your branch from `main`. 2. If you've added code that should be tested, add tests 3. If you've changed APIs, update the documentation. -4. Ensure the test suite passes. -5. Make sure your code lints. +4. Format the repository with `yarn format`. +5. Ensure the test suite passes and `yarn lint` succeeds. 6. If you haven't already, complete the Contributor License Agreement ("CLA"). +`yarn format-check` checks all maintained JavaScript and documentation, C-family, +Kotlin, and Python sources without modifying them. Running all formatters requires +Node and Yarn, Java 17 or newer, and Python 3 with pip. + ## Contributor License Agreement ("CLA") + In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: ## Issues + We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. @@ -30,5 +39,6 @@ disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. ## License + By contributing to yoga, you agree that your contributions will be licensed under its MIT license. diff --git a/javascript/package.json b/javascript/package.json index 32e77d586b..80854617af 100644 --- a/javascript/package.json +++ b/javascript/package.json @@ -24,8 +24,8 @@ "scripts": { "benchmark": "just benchmark --config just.config.cjs", "build": "just build --config just.config.cjs", - "clang-format": "just clang-format --config just.config.cjs", - "clang-format:fix": "just clang-format --fix --config just.config.cjs", + "clang-format": "yarn --cwd .. format-check-cpp", + "clang-format:fix": "yarn --cwd .. format-cpp", "clean": "just clean --config just.config.cjs", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -43,7 +43,6 @@ "@types/which": "^3.0.0", "@yogalayout/cmake-bin": "3.28.0-1", "babel-register-esm": "^1.2.5", - "clang-format": "^1.8.0", "glob": "^10.4.2", "jest": "^29.3.1", "just-scripts": "^2.1.0", diff --git a/package.json b/package.json index b40b23e4b2..fab50fe141 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,16 @@ "version": "0.0.0", "private": true, "scripts": { + "format": "yarn format-javascript && yarn format-cpp && yarn format-kotlin && yarn format-python", + "format-check": "yarn format-check-javascript && yarn format-check-cpp && yarn format-check-kotlin && yarn format-check-python", + "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-kotlin": "node ./scripts/format-kotlin.js --check", + "format-check-python": "node ./scripts/format-python.js --check", + "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", + "format-python": "node ./scripts/format-python.js", "lint": "eslint .", "lint:fix": "eslint . --fix", "tsc": "yarn workspaces run tsc", @@ -21,6 +31,9 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-jest": "^27.1.7", "eslint-plugin-prettier": "^5.5.6", + "fb-dotslash": "0.5.8", + "glob": "^10.4.2", + "ktfmt": "0.59.0", "prettier": "3.9.4", "typescript": "5.0.4" }, diff --git a/scripts/clang-format b/scripts/clang-format new file mode 100755 index 0000000000..c41c09f07f --- /dev/null +++ b/scripts/clang-format @@ -0,0 +1,52 @@ +#!/usr/bin/env dotslash + +// 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. + +{ + "name": "clang-format 21.1.2", + "platforms": { + "linux-aarch64": { + "size": 1725525, + "hash": "sha256", + "digest": "f66d2bcf98df1373df6ab4544a2b881e9816985b606e1144e4c77dc8ac87b826", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/c5/83/61fadfa8d62a288d778e0a1ad2f73b01abca64574ee34c5d6d078e0821da/clang_format-21.1.2-py2.py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format" + }, + "linux-x86_64": { + "size": 1805060, + "hash": "sha256", + "digest": "6d7caf74fe89154258ddfd63984c98ffe902ef98f013ac517178fc44d72861ff", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/fb/ac/3c04772acc0257f5730e83adb542b2603c1a62d1315010ab593a980af404/clang_format-21.1.2-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format" + }, + "macos-aarch64": { + "size": 1458874, + "hash": "sha256", + "digest": "fc034652dee24583633177d800bc9deebcc9c65eb7ab53b25bbd0fbd443392a9", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/ba/90/b8230efcff90a8543da3fb7fc09d7077afebaba019eceb1686d4db94cac3/clang_format-21.1.2-py2.py3-none-macosx_11_0_arm64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format" + }, + "macos-x86_64": { + "size": 1440163, + "hash": "sha256", + "digest": "00498efb43d60d7ac4195362009a79936d26145a9a90cdfa7a6013a62ab3c40c", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/7d/97/0bb5a6866dfb5f55f7e6ca79466cb0b0081fccbc9f57887949ff23b5c38a/clang_format-21.1.2-py2.py3-none-macosx_10_9_x86_64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format" + }, + "windows-x86_64": { + "size": 1426244, + "hash": "sha256", + "digest": "c98e195a50c0fa40bb058449511b1b681ca7ad553579aa32425f0cfeca8d81ce", + "providers": [{"type": "http", "url": "https://files.pythonhosted.org/packages/fe/0d/3b9c6a41a9eed2d45431d91c0e8608da315cd44d0c24c517bfb686db4b6b/clang_format-21.1.2-py2.py3-none-win_amd64.whl"}], + "format": "zip", + "path": "clang_format/data/bin/clang-format.exe" + } + } +} diff --git a/scripts/clang-format.js b/scripts/clang-format.js new file mode 100644 index 0000000000..2922963032 --- /dev/null +++ b/scripts/clang-format.js @@ -0,0 +1,93 @@ +/** + * 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 {findMetaTool, REPO_ROOT} = require('./format-utils'); +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const {globSync} = require('glob'); + +const OSS_CLANG_FORMAT_DOTSLASH = path.join(__dirname, 'clang-format'); +const GENERATED_MARKER = Buffer.from('@' + 'generated'); +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}'; +const IGNORE = [ + '**/.cxx/**', + '**/build/**', + '**/node_modules/**', + '**/Pods/**', + 'lib/**', +]; + +function findClangFormat() { + if (process.env.CLANG_FORMAT != null && process.env.CLANG_FORMAT !== '') { + return {command: process.env.CLANG_FORMAT, prefixArguments: []}; + } + return ( + findMetaTool('tools', 'third-party', 'clang-format', 'clang-format') ?? { + command: require('fb-dotslash'), + prefixArguments: [OSS_CLANG_FORMAT_DOTSLASH], + } + ); +} + +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) { + console.warn(`Unable to inspect ${file}: ${String(error)}`); + return false; + } finally { + if (fd != null) { + fs.closeSync(fd); + } + } +} + +function main() { + const check = process.argv.includes('--check'); + const files = globSync(SOURCE_GLOB, { + cwd: REPO_ROOT, + ignore: IGNORE, + }).filter(file => !isGenerated(file)); + const clangFormat = findClangFormat(); + let exitStatus = 0; + + for (let index = 0; index < files.length; index += MAX_FILES_PER_PROCESS) { + const result = spawnSync( + clangFormat.command, + [ + ...clangFormat.prefixArguments, + ...(check ? ['--dry-run', '--Werror'] : ['-i']), + ...files.slice(index, index + MAX_FILES_PER_PROCESS), + ], + {cwd: REPO_ROOT, stdio: 'inherit'}, + ); + if (result.error != null) { + throw result.error; + } + if (result.signal != null) { + throw new Error(`clang-format was terminated by ${result.signal}`); + } + if (result.status !== 0) { + exitStatus = result.status ?? 1; + } + } + process.exitCode = exitStatus; +} + +main(); diff --git a/scripts/format-kotlin.js b/scripts/format-kotlin.js new file mode 100644 index 0000000000..dace0d87c7 --- /dev/null +++ b/scripts/format-kotlin.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 {findJava, REPO_ROOT} = require('./format-utils'); +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const {globSync} = require('glob'); + +const GENERATED_MARKER = Buffer.from('@' + 'generated'); +const MAX_HEADER_BYTES = 4096; +const MAX_FILES_PER_PROCESS = 100; + +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) { + console.warn(`Unable to inspect ${file}: ${String(error)}`); + return false; + } finally { + if (fd != null) { + fs.closeSync(fd); + } + } +} + +function main() { + const check = process.argv.includes('--check'); + const ktfmt = + process.env.KTFMT != null && process.env.KTFMT !== '' + ? {command: process.env.KTFMT, prefixArguments: []} + : (() => { + const java = findJava(17); + if (java == null) { + throw new Error( + 'Kotlin formatting requires Java 17 or newer. Install a JDK or set JAVA=/path/to/java.', + ); + } + const jar = + process.env.KTFMT_JAR != null && process.env.KTFMT_JAR !== '' + ? process.env.KTFMT_JAR + : require.resolve('ktfmt/lib/ktfmt.jar'); + return {command: java, prefixArguments: ['-jar', jar]}; + })(); + const files = globSync('**/*.{kt,kts}', { + cwd: REPO_ROOT, + ignore: ['**/.gradle/**', '**/build/**', '**/node_modules/**'], + }).filter(file => !isGenerated(file)); + let exitStatus = 0; + + for (let index = 0; index < files.length; index += MAX_FILES_PER_PROCESS) { + const result = spawnSync( + ktfmt.command, + [ + ...ktfmt.prefixArguments, + '--do-not-remove-unused-imports', + ...(check ? ['--dry-run', '--set-exit-if-changed'] : []), + ...files.slice(index, index + MAX_FILES_PER_PROCESS), + ], + {cwd: REPO_ROOT, stdio: 'inherit'}, + ); + if (result.error != null) { + throw result.error; + } + if (result.signal != null) { + throw new Error(`ktfmt was terminated by ${result.signal}`); + } + if (result.status !== 0) { + exitStatus = result.status ?? 1; + } + } + process.exitCode = exitStatus; +} + +main(); diff --git a/scripts/format-python.js b/scripts/format-python.js new file mode 100644 index 0000000000..0a83e3c917 --- /dev/null +++ b/scripts/format-python.js @@ -0,0 +1,150 @@ +/** + * 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 {findMetaTool, REPO_ROOT} = require('./format-utils'); +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const {globSync} = require('glob'); + +const RUFF_VERSION = '0.14.0'; +const RUFF_ROOT = path.join( + REPO_ROOT, + 'node_modules', + '.cache', + 'yoga-format', + `ruff-${RUFF_VERSION}`, +); +const PYTHON_GLOB = '**/*.py'; +const IGNORE = ['**/build/**', '**/node_modules/**']; + +function run(command, arguments_, options = {}) { + const result = spawnSync(command, arguments_, { + cwd: REPO_ROOT, + stdio: options.quiet === true ? 'ignore' : 'inherit', + ...options, + env: {...(options.env ?? process.env), PWD: REPO_ROOT}, + }); + if (result.error != null) { + if ( + options.allowMissingCommand === true && + result.error.code === 'ENOENT' + ) { + return 1; + } + throw result.error; + } + if (result.signal != null) { + throw new Error(`${command} was terminated by ${result.signal}`); + } + return result.status ?? 1; +} + +function findPython() { + const candidates = + process.platform === 'win32' + ? [ + ['py', ['-3']], + ['python', []], + ] + : [ + ['python3', []], + ['python', []], + ]; + return ( + candidates.find( + ([command, prefixArguments]) => + run( + command, + [ + ...prefixArguments, + '-c', + 'import sys; raise SystemExit(sys.version_info.major != 3)', + ], + {allowMissingCommand: true, quiet: true}, + ) === 0, + ) ?? null + ); +} + +function runRuff(command, prefixArguments, check, environment) { + const files = globSync(PYTHON_GLOB, {cwd: REPO_ROOT, ignore: IGNORE}); + return run( + command, + [...prefixArguments, 'format', ...(check ? ['--check'] : []), ...files], + {env: environment}, + ); +} + +function main() { + const check = process.argv.includes('--check'); + if (process.env.RUFF != null && process.env.RUFF !== '') { + process.exitCode = runRuff(process.env.RUFF, [], check, process.env); + return; + } + + const metaRuff = findMetaTool('tools', 'third-party', 'ruff', 'ruff'); + if (metaRuff != null) { + process.exitCode = runRuff( + metaRuff.command, + metaRuff.prefixArguments, + check, + process.env, + ); + return; + } + + const python = findPython(); + if (python == null) { + throw new Error('Python formatting requires Python 3 with pip.'); + } + const [command, prefixArguments] = python; + const pythonPath = [RUFF_ROOT, process.env.PYTHONPATH] + .filter(Boolean) + .join(path.delimiter); + const environment = {...process.env, PYTHONPATH: pythonPath}; + if ( + run( + command, + [ + ...prefixArguments, + '-c', + `from importlib.metadata import version; raise SystemExit(version('ruff') != '${RUFF_VERSION}')`, + ], + {env: environment, quiet: true}, + ) !== 0 + ) { + fs.mkdirSync(RUFF_ROOT, {recursive: true}); + const installStatus = run(command, [ + ...prefixArguments, + '-m', + 'pip', + 'install', + '--disable-pip-version-check', + '--only-binary=:all:', + `--target=${RUFF_ROOT}`, + `ruff==${RUFF_VERSION}`, + ]); + if (installStatus !== 0) { + process.exitCode = installStatus; + return; + } + } + process.exitCode = runRuff( + command, + [...prefixArguments, '-m', 'ruff'], + check, + environment, + ); +} + +main(); diff --git a/scripts/format-utils.js b/scripts/format-utils.js new file mode 100644 index 0000000000..1140dc01e2 --- /dev/null +++ b/scripts/format-utils.js @@ -0,0 +1,82 @@ +/** + * 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 fs = require('node:fs'); +const path = require('node:path'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const FBSOURCE_ROOT = path.resolve(REPO_ROOT, '../..'); + +function findMetaTool(...relativePath) { + const manifest = path.join(FBSOURCE_ROOT, ...relativePath); + const dotslash = ['/usr/bin/dotslash', '/usr/local/bin/dotslash'].find( + candidate => fs.existsSync(candidate), + ); + return dotslash != null && fs.existsSync(manifest) + ? {command: dotslash, prefixArguments: [manifest]} + : 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 !== '') { + const version = javaMajorVersion(process.env.JAVA); + if (version == null) { + throw new Error( + `JAVA=${process.env.JAVA} did not run or report a recognizable Java version.`, + ); + } + if (version < minimumVersion) { + throw new Error( + `JAVA=${process.env.JAVA} is Java ${version}; Java ${minimumVersion} or newer is required.`, + ); + } + return process.env.JAVA; + } + + const candidates = ['java']; + if (process.platform === 'darwin') { + for (const version of [17, 21]) { + const result = spawnSync( + '/usr/libexec/java_home', + ['-v', String(version)], + { + encoding: 'utf8', + }, + ); + if (result.status === 0 && result.stdout.trim() !== '') { + candidates.unshift(path.join(result.stdout.trim(), 'bin', 'java')); + } + } + } else { + candidates.unshift( + '/usr/local/java-runtime/impl/17/bin/java', + '/usr/local/java-runtime/17/bin/java', + '/usr/local/java-runtime/impl/21/bin/java', + '/usr/local/java-runtime/21/bin/java', + ); + } + + return ( + candidates.find(command => javaMajorVersion(command) >= minimumVersion) ?? + null + ); +} + +module.exports = {findJava, findMetaTool, REPO_ROOT}; diff --git a/website/blog/2023-06-30-announcing-yoga-2.0.md b/website/blog/2023-06-30-announcing-yoga-2.0.md index ea706f9dc0..2de7e8caf0 100644 --- a/website/blog/2023-06-30-announcing-yoga-2.0.md +++ b/website/blog/2023-06-30-announcing-yoga-2.0.md @@ -8,13 +8,13 @@ authors: We are excited to announce a new major (breaking) version of Yoga. This release contains 198 new commits from 64 contributors, and includes the first significant changes to Yoga in open-source since 2018. While there has been a [long gap in development](https://github.com/facebook/yoga/issues/1151), the React Native team believes Yoga is a critical tool in delivering the future of React, and we have resumed development of the engine. This release of Yoga is focused on: + 1. Delivering the features we have shipped to React Native to standalone Yoga users 2. Updating toolchains, packaging, and build systems to make Yoga easier to consume 3. API changes to allow us to evolve the engine towards conformance Yoga continuously ships to thousands of surfaces across multiple frameworks at Meta. This version of Yoga corresponds to the version which will be included in React Native 0.73 shipping this fall. - ## Flexbox gap The most significant new feature for users on an older stable release of Yoga is the addition of Flexbox gap support. This powers `gap`, `rowGap`, and `columnGap` in React Native 0.71. @@ -34,12 +34,12 @@ node.setGap(YogaGutter.ROW, 2.0f); node.setGap(Gutter.Row, 2); ``` - ## Toolchain Meta uses [Buck](https://buck.build/) across its monorepo, but we recognize that Buck has acted as a barrier to be able to use Yoga outside of Meta. Yoga no longer ships build logic for Buck to open-source. We have instead added over 20 new validation jobs to GitHub Actions to continually validate that Yoga builds correctly in common systems and scenarios where Yoga is used in OSS. New toolchain support includes: + 1. A reference CMake build for Yoga and its unit tests 1. A modern Gradle build and published AARs 1. Compatibility with XCode 14.3+ @@ -90,7 +90,6 @@ Two variants are shipped: Both are about 45KB when gzipped. - WebAssembly binaries must be asynchronously compiled and loaded in Chrome. In the absence of universal support for top-level await, we have made the breaking change to require explicitly asynchronously loading Yoga before using it. ```ts @@ -101,8 +100,7 @@ const node = Yoga.Node.create(); node.setAlignContent(Align.Center); ``` - -The previous behavior of blocking to load the binary can be replicated by importing from the `yoga-layout/sync` entrypoint, but this is not recommended for new usages, and does not allow using WebAssembly on browsers. +The previous behavior of blocking to load the binary can be replicated by importing from the `yoga-layout/sync` entrypoint, but this is not recommended for new usages, and does not allow using WebAssembly on browsers. ```ts import Yoga, {ALIGN_CENTER} from 'yoga-layout/sync'; @@ -119,19 +117,22 @@ import {loadYoga} from 'yoga-layout/wasm-async'; > Note: the `yoga-layout` package requires your bundler and typechecker to configured to be able to follow the package `exports `field. - ## Deprecations and removals ### YogaKit and YogaLayout ViewGroup + We are deprecating, `YogaKit` and the `YogaLayout` ViewGroup. These libraries allow initegrating Yoga directly with UIKit and the Android view system, but are not widely used by Meta in production. We are instead focusing on higher-level libraries using Yoga like [Litho](https://fblitho.com/) and [React Native](https://reactnative.dev/). Because we aren’t in a place to continue development, or validate contributions, we are discontinuing development. These libraries will not receive future updates beyond the Yoga `release-v2.0` branch. ### UseLegacyStretchBehaviour + The functions to manipulate `UseLegacyStretchBehaviour` have been deprecated. Previous users of the API should now set an appropriate errata level, like `YGErrataAll` to opt-out of all future conformance fixes. ### C# bindings + C# bindings were contributed to the Yoga repo but have since degraded. The bindings have not had working build validation, or a consistent contributor. We have removed them from the Yoga repo, but we will continue to provide a public C ABI for others to build bindings on top of. ### Private C++ APIs + Yoga’s header structure has historically allowed the inclusion of concrete internal structures like `YGStyle` or `YGNode`. We will begin to enforce that users instead rely on the public C APIs provided by `#include `. Other C++ APIs may change without notice. ```cpp @@ -150,12 +151,10 @@ YGConfig config{yogaLogger_}; config.pointScaleFactor = 1.0f; ``` - ### C++ 11 Support Yoga now requires a compiler which supports C++ 14. This will likely be bumped to C++ 17 in a future minor release. - ## How do I get it? For users who don’t want to build from source, new Yoga packages have been published to the npmjs registry, Maven Central, and CocoaPods. @@ -179,7 +178,6 @@ dependencies { pod 'Yoga', '~> 2.0.0' ``` - ## Acknowledgements Yoga 2.0 contains major external contributions from [@intergalacticspacehighway](https://github.com/intergalacticspacehighway), [@jacobp100](https://github.com/jacobp100), [@jeetiss](https://github.com/jeetiss) and [@nicoburns](https://github.com/nicoburns). diff --git a/website/blog/2024-06-24-announcing-yoga-3.1.md b/website/blog/2024-06-24-announcing-yoga-3.1.md index 9bb3e83b39..2870cd222f 100644 --- a/website/blog/2024-06-24-announcing-yoga-3.1.md +++ b/website/blog/2024-06-24-announcing-yoga-3.1.md @@ -38,26 +38,26 @@ Percentages in Yoga do not always act consistently with browsers when a definite `} /> - - ## Alignment changes to overflowed containers Yoga has made [several](https://github.com/facebook/yoga/commit/932361cdbf5864a2e6929b68154b4ada2ac586d9) [fixes](https://github.com/facebook/yoga/commit/6f10656868c2eca9b2eae55ad754e01e70565f69) to how flex-children of overflowed containers are aligned when using `justify-content`, `align-content`, or `margin: "auto"`. This includes some cases where adding an alignment keyword could cause items to overlap, or padding to be removed. - + +> + + `} /> - ## Fixes for regressions in Yoga 3.0 We fixed a couple of regressions added in Yoga 3.0, around percentage insets applied to some absolute containers ([#1657](https://github.com/facebook/yoga/issues/1657)), and start/end resolution when physical edge styles are also present ([#1658](https://github.com/facebook/yoga/issues/1658)). diff --git a/website/blog/2024-12-02-announcing-yoga-3.2.md b/website/blog/2024-12-02-announcing-yoga-3.2.md index 9599584332..3e4171072e 100644 --- a/website/blog/2024-12-02-announcing-yoga-3.2.md +++ b/website/blog/2024-12-02-announcing-yoga-3.2.md @@ -47,29 +47,30 @@ To preserve compatibility, Yoga nodes default to `box-sizing: border-box`, even `} /> - - ## `display: contents` Yoga nodes [may now be set to `display: contents`](https://github.com/facebook/yoga/commit/68bb2343d2b470962065789d09016bba8e785340) to remove them from the layout flow, while preserving and hoisting the node's children. This may be used by the higher level UI framework to allow more easily composing wrapper components (such as those which may need to handle events, without influencing child layout). Thanks [@j-piasecki](https://github.com/j-piasecki) for the contribution! - + +> + + + `} /> - ## Removal of legacy absolute positioning Yoga 3.0 introduced a new algorithm used for absolute positioning. This algorithm is more correct than the one previously used, but led to observed compatibility issues with existing code, so we left the option to disable it via the `AbsolutePositioningIncorrect` erratum (enabled by default in frameworks like React Native). Yoga 3.2 removes the legacy absolute positioning path, but ports over the main compatibility quirk under a new erratum `AbsolutePositionWithoutInsetsExcludesPadding` (where the previous incorrect behavior would omit padding when a position was not specified on the absolute node). Errata users should see more correct absolute positioning behavior, while preserving compatibility with existing code. diff --git a/website/blog/yoga-3-release/2024-03-14-announcing-yoga-3.0.md b/website/blog/yoga-3-release/2024-03-14-announcing-yoga-3.0.md index 8b30143737..42fc9fd7ad 100644 --- a/website/blog/yoga-3-release/2024-03-14-announcing-yoga-3.0.md +++ b/website/blog/yoga-3-release/2024-03-14-announcing-yoga-3.0.md @@ -21,15 +21,13 @@ Yoga 3.0 is a new major (breaking) version of Yoga, used by React Native 0.74. We added full support for the `static` position type which has existed in an incomplete state for some time now. With this release `static` is now web-compliant in the context of Flexbox. Some things that were added/changed: -* The default position type is now `relative` [again](https://github.com/facebook/yoga/commit/fc88b2f774f0ab9090d7ca15de6680f26d7285ad) and not `static`. This should not have any effect on layout as the previously introduced `YGPositionTypeStatic` was not being used within Yoga, so it behaved just like `relative`. -* `static` nodes ignore insets (`left`, `right`, `top`, `bottom`, etc.) -* The idea of a[ containing block](../../docs/advanced/containing-block) was introduced. For `absolute` nodes this is usually the nearest non-`static` ancestor. For every other position type this is just the parent since Yoga is a Flexbox implementation. -* A new public API `YGNodeSetAlwaysFormsContainingBlock` which takes a boolean indicating if the node should always form a containing block for any descendant. This is useful for properly supporting things like [transforms](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transforms), which will force the node to form a containing block but is outside the scope of Yoga. - +- The default position type is now `relative` [again](https://github.com/facebook/yoga/commit/fc88b2f774f0ab9090d7ca15de6680f26d7285ad) and not `static`. This should not have any effect on layout as the previously introduced `YGPositionTypeStatic` was not being used within Yoga, so it behaved just like `relative`. +- `static` nodes ignore insets (`left`, `right`, `top`, `bottom`, etc.) +- The idea of a[ containing block](../../docs/advanced/containing-block) was introduced. For `absolute` nodes this is usually the nearest non-`static` ancestor. For every other position type this is just the parent since Yoga is a Flexbox implementation. +- A new public API `YGNodeSetAlwaysFormsContainingBlock` which takes a boolean indicating if the node should always form a containing block for any descendant. This is useful for properly supporting things like [transforms](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transforms), which will force the node to form a containing block but is outside the scope of Yoga. `position: static` affects some concerns outside of layout, like stacking context. The full set of behaviors is enabled in the React Native New Architecture. - ## Better support for absolute positioning There were a variety of bugs with how `absolute` nodes were positioned under various `Justify` and `Align` values. Most of these bugs only ever manifested themselves with certain paddings, margins, and borders so the following examples mix those up to illustrate the differences. Additionally, the following positioning examples all share this core style: @@ -176,7 +174,6 @@ child: { - There were other fixes not specifically mentioned above. Because this change may result in layout differences for many real-world scenarios, Yoga may be configured to prefer the legacy behavior, by setting the `AbsolutePositioningIncorrect` erratum. This means this fix is not enabled by default in frameworks like React Native, in order to preserve compatibility. Additionally, Yoga will now correctly account for padding when calculating the size of absolutely positioned nodes with percentage lengths. @@ -224,10 +221,8 @@ child: { - Just like with positioning, this fix may result in layout differences in many cases. Setting the `AbsolutePercentAgainstInnerSize` erratum will preserve the legacy behavior, which is set by default in frameworks like React Native. - ## Better support for multiline containers Yoga now offers better support for Flexbox containers which span multiple lines. @@ -240,7 +235,6 @@ Yoga now supports `align-content: space-evenly`, contributed by [@nicoburns](htt Yoga now correctly supports the combination of `align-content` and `align-items` when both cause changes to alignment. - @@ -252,7 +246,6 @@ Yoga now correctly supports the combination of `align-content` and `align-items`
```jsx - + }}> https://github.com/facebook/yoga/issues/1008 - Yoga now correctly supports `min-height` set on a multiline container. @@ -332,8 +323,7 @@ Yoga now correctly supports `min-height` set on a multiline container. justifyContent: 'center', padding: 10, gap: 10, - }} -> + }}> - ## Correct handling of logical edges in row-reverse containers Yoga would previously incorrectly reverse `start` and `end` edges, when operating on: @@ -399,16 +388,14 @@ We determined that few enough surfaces are impacted by this bug to enable the co margin: 10, width: 200, height: 100, - }} -> + }}> + }}> @@ -465,8 +451,7 @@ Yoga previously calculated an incorrect main-axis size for containers which spec padding: 10, justifyContent: 'space-between', alignItems: 'center', - }} -> + }}> - ``` @@ -505,7 +489,6 @@ Yoga previously calculated an incorrect main-axis size for containers which spec This change was observed to impact existing layouts rarely enough to enable globally, and while this change was not present in Yoga 2.0, it was present in the version of Yoga ultimately shipped in React Native 0.73. - ## Distribution as an ES Module Yoga’s previous JavaScript package exported a convoluted matrix of different binaries, across asm.js and wasm, sync vs async, and browser vs node. @@ -514,7 +497,6 @@ When it came time to look at adding ES Module support into the mix, we decided t The underlying binary is still distributed as a JavaScript blob of base64 encoded WebAssembly, usable across different environments and bundlers. - ## Java reliability improvements Several crashes have been fixed in Yoga’s Java bindings: @@ -522,8 +504,6 @@ Several crashes have been fixed in Yoga’s Java bindings: 1. Yoga for Java [no longer performs an invalid read](https://github.com/facebook/yoga/commit/38ad93c87baa2127872892a6da674fc93e84f4b8) if a message is logged 2. Yoga for Java now makes [more efficient use of JNI references](https://github.com/facebook/yoga/commit/49fbd406b62566d8b243297725d3485d9de9d442). This can help avoid app crashes in large trees, or interaction with other layout systems also using JNI. - - ## Infrastructure changes Yoga’s implementation now targets a well-supported subset of C++ 20. Supported toolchains include: @@ -538,10 +518,8 @@ Yoga’s reference Android build and accompanying artifacts now target Android S Yoga now compiles cleanly against higher warning levels, such as `-Wextra` and `-Wconversion` in Clang/GCC, and `/W4` in MSVC. - ## Deprecations and removals - ### Changes to C++ APIs Yoga’s previous structure made it easy to intermingle Yoga’s public APIs, and Yoga’s C++ implementation structures. The boundary between these two has been made firmer. @@ -549,15 +527,12 @@ Yoga’s previous structure made it easy to intermingle Yoga’s public APIs, an 1. Every top-level header is now a public API 2. All public APIs may be used from C, C++, Objective C, and Swift - We have made some minor changes to this public API, which will require changes for Yoga integrators. The most significant is an increased const-correctness, which may require mechanical changes to measure functions. Yoga’s internal implementation has seen more radical changes. - ### Removal of `UseLegacyStretchBehaviour` APIs related to `UseLegacyStretchBehaviour` were deprecated as part of Yoga 2.0, and have now been removed. Users of `UseLegacyStretchBehaviour` should most often [set their errata level](../../docs/getting-started/configuring-yoga#layout-conformance-and-yoga-errata) to `All` to opt-out of future conformance fixes. - ### Removal of YogaKit and the YogaLayout ViewGroup Yoga previously provided direct integrations with UIKit, and the Android View System. These were deprecated as part of Yoga 2.0 and are no longer published as part of Yoga 3.0. @@ -577,7 +552,7 @@ Yoga includes a reference CMake build, and has official bindings published acros { "dependencies": { "yoga-layout": "^3.0.0" - } + } } ``` diff --git a/website/docs/styling/index.md b/website/docs/styling/index.md index 0e983bf3f4..a256001a9f 100644 --- a/website/docs/styling/index.md +++ b/website/docs/styling/index.md @@ -30,6 +30,7 @@ Yoga acts as if [`box-sizing: 'border-box'`](https://developer.mozilla.org/en-US ## Edges Margin, padding, position, and border, are set against an Edge, which may be: + 1. Relative to writing direction (start/end) 2. A physical edge (top/right/left/bottom) 3. A collection of edges (vertical/horizontal/all) diff --git a/yarn.lock b/yarn.lock index 2100b5d43a..39a17ac335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3455,11 +3455,6 @@ async-settle@^2.0.0: dependencies: async-done "^2.0.0" -async@^3.2.3: - version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" @@ -3943,15 +3938,6 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== -clang-format@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.8.0.tgz#7779df1c5ce1bc8aac1b0b02b4e479191ef21d46" - integrity sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw== - dependencies: - async "^3.2.3" - glob "^7.0.0" - resolve "^1.1.6" - clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" @@ -5394,6 +5380,11 @@ faye-websocket@^0.11.3: dependencies: websocket-driver ">=0.5.1" +fb-dotslash@0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/fb-dotslash/-/fb-dotslash-0.5.8.tgz#c5ef3dacd75e1ddb2197c367052464ddde0115f5" + integrity sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA== + fb-watchman@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -7295,6 +7286,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== + last-run@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/last-run/-/last-run-2.0.0.tgz#f82dcfbfce6e63d041bd83d64c82e34cdba6572e"