From a5be3ce9353ae65f2b78352812fe8de0dd3737a6 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Fri, 10 Jul 2026 10:35:36 -0700 Subject: [PATCH 1/2] Consolidate package checks into one Jest test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Simplify/unify various mechanisms for the monorepo's package invariants. These checks were previously scattered: - `private/monorepo-tests` package (manifest field checks) - `.github/workflow-scripts/lint_files.sh` (`.npmignore` ban) - `react-native/eslint-plugin-monorepo` (manifest field checks — duplicated) This folds everything into `scripts/monorepo-tests/__tests__/check-packages-test.js`. A plain Jest test is the most extensible home for future checks. We also drop the `jsonc-eslint-parser` dependency. Changelog: [Internal] Differential Revision: D111471313 --- .eslintrc.js | 6 -- .github/workflow-scripts/lint_files.sh | 12 --- .github/workflows/test-all.yml | 3 - private/eslint-plugin-monorepo/index.js | 1 - .../__tests__/react-native-manifest-test.js | 92 ------------------- .../rules/react-native-manifest.js | 85 ----------------- .../no-dependencies-in-root-package-test.js | 31 ------- ...pendencies-in-react-native-package-test.js | 33 ------- .../no-dev-dependencies-in-tester-package.js | 33 ------- private/monorepo-tests/package.json | 5 - .../__tests__/check-packages-test.js | 42 +++++++++ 11 files changed, 42 insertions(+), 301 deletions(-) delete mode 100755 .github/workflow-scripts/lint_files.sh delete mode 100644 private/eslint-plugin-monorepo/rules/__tests__/react-native-manifest-test.js delete mode 100644 private/eslint-plugin-monorepo/rules/react-native-manifest.js delete mode 100644 private/monorepo-tests/__tests__/no-dependencies-in-root-package-test.js delete mode 100644 private/monorepo-tests/__tests__/no-dev-dependencies-in-react-native-package-test.js delete mode 100644 private/monorepo-tests/__tests__/no-dev-dependencies-in-tester-package.js delete mode 100644 private/monorepo-tests/package.json create mode 100644 scripts/monorepo-tests/__tests__/check-packages-test.js diff --git a/.eslintrc.js b/.eslintrc.js index 4ef0585b7674..f236d21a44f2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -61,12 +61,6 @@ module.exports = { files: ['package.json'], parser: 'jsonc-eslint-parser', }, - { - files: ['package.json'], - rules: { - '@react-native/monorepo/react-native-manifest': 'error', - }, - }, { files: ['flow-typed/**/*.js', 'packages/react-native/flow/**/*'], rules: { diff --git a/.github/workflow-scripts/lint_files.sh b/.github/workflow-scripts/lint_files.sh deleted file mode 100755 index 9563d7dafb57..000000000000 --- a/.github/workflow-scripts/lint_files.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# 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. - -set -e - -if git ls-files | grep -E '\.npmignore$'; then - echo "Error: Found unexpected .npmignore file(s). Please use package.json#files instead." - exit 1 -fi diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 1f49896cbf36..ef4e0a50029f 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -453,9 +453,6 @@ jobs: uses: ./.github/actions/setup-node - name: Install dependencies uses: ./.github/actions/yarn-install - - name: Lint file structure - shell: bash - run: ./.github/workflow-scripts/lint_files.sh - name: Run shellcheck shell: bash run: ./.github/workflow-scripts/analyze_scripts.sh diff --git a/private/eslint-plugin-monorepo/index.js b/private/eslint-plugin-monorepo/index.js index 6b34791bc03b..7ad4b026eb82 100644 --- a/private/eslint-plugin-monorepo/index.js +++ b/private/eslint-plugin-monorepo/index.js @@ -15,7 +15,6 @@ exports.rules = { 'no-react-named-type-imports': require('./rules/no-react-named-type-imports'), 'no-react-native-imports': require('./rules/no-react-native-imports'), 'no-react-node-imports': require('./rules/no-react-node-imports'), - 'react-native-manifest': require('./rules/react-native-manifest'), 'require-extends-error': require('./rules/require-extends-error'), 'sort-imports': require('./rules/sort-imports'), 'valid-flow-typed-signature': require('./rules/valid-flow-typed-signature'), diff --git a/private/eslint-plugin-monorepo/rules/__tests__/react-native-manifest-test.js b/private/eslint-plugin-monorepo/rules/__tests__/react-native-manifest-test.js deleted file mode 100644 index c0ec67da127b..000000000000 --- a/private/eslint-plugin-monorepo/rules/__tests__/react-native-manifest-test.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 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. - * - * @format - * @noflow - */ - -'use strict'; - -const rule = require('../react-native-manifest.js'); -const {RuleTester} = require('eslint'); - -const ruleTester = new RuleTester({ - parser: require.resolve('jsonc-eslint-parser'), -}); - -ruleTester.run('react-native-manifest', rule, { - valid: [ - { - code: JSON.stringify({ - name: '@react-native/package-name', - }), - }, - { - code: JSON.stringify({ - name: '@react-native/package-name', - dependencies: { - dependencyA: '1.0.0', - }, - devDependencies: { - dependencyB: '1.0.0', - }, - }), - }, - { - code: JSON.stringify({ - name: '@react-native/monorepo', - devDependencies: { - dependencyB: '1.0.0', - }, - }), - }, - { - code: JSON.stringify({ - name: 'react-native', - dependencies: { - dependencyA: '1.0.0', - }, - }), - }, - ], - invalid: [ - { - code: JSON.stringify({ - name: '@react-native/monorepo', - dependencies: { - dependencyA: '1.0.0', - }, - }), - errors: [ - { - messageId: 'propertyDisallowed', - data: { - property: 'dependencies', - describe: - "Declare 'dependencies' in `packages/react-native/package.json`.", - }, - }, - ], - }, - { - code: JSON.stringify({ - name: 'react-native', - devDependencies: { - dependencyA: '1.0.0', - }, - }), - errors: [ - { - messageId: 'propertyDisallowed', - data: { - property: 'devDependencies', - describe: "Declare 'devDependencies' in `/package.json`.", - }, - }, - ], - }, - ], -}); diff --git a/private/eslint-plugin-monorepo/rules/react-native-manifest.js b/private/eslint-plugin-monorepo/rules/react-native-manifest.js deleted file mode 100644 index ff073946ac17..000000000000 --- a/private/eslint-plugin-monorepo/rules/react-native-manifest.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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. - * - * @format - * @noflow - */ - -'use strict'; - -/** - * React Native's monorepo requires that "devDependencies" be declared at the - * root package.json and "dependencies" in the `react-native` package.json to - * permit the ability to segment dependent workspaces into 1) a development - * workspace root (which depends on the monorepo) and 2) a production workspace - * (which depends on the `react-native` package). - */ -const PACKAGE_CONSTRAINTS = { - '@react-native/monorepo': { - disallowed: [ - { - property: 'dependencies', - describe: - "Declare 'dependencies' in `packages/react-native/package.json`.", - }, - ], - }, - 'react-native': { - disallowed: [ - { - property: 'devDependencies', - describe: "Declare 'devDependencies' in `/package.json`.", - }, - ], - }, -}; - -module.exports = { - meta: { - type: 'problem', - docs: { - description: 'Enforce react-native manifest constraints', - }, - messages: { - propertyDisallowed: - "'{{property}}' is disallowed in this file. {{describe}}", - }, - schema: [], - }, - - create(context) { - // @see https://www.npmjs.com/package/jsonc-eslint-parser - if (!context.parserServices.isJSON) { - return {}; - } - return { - 'JSONExpressionStatement > JSONObjectExpression'(node) { - const propertyNodes = {}; - for (const propertyNode of node.properties) { - propertyNodes[propertyNode.key.value] = propertyNode; - } - - const name = propertyNodes.name?.value?.value; - const constraints = PACKAGE_CONSTRAINTS[name]; - if (constraints == null) { - return; - } - - for (const {property, describe} of constraints.disallowed) { - const propertyNode = propertyNodes[property]; - if (propertyNode == null) { - continue; - } - context.report({ - node: propertyNode, - messageId: 'propertyDisallowed', - data: {property, describe}, - }); - } - }, - }; - }, -}; diff --git a/private/monorepo-tests/__tests__/no-dependencies-in-root-package-test.js b/private/monorepo-tests/__tests__/no-dependencies-in-root-package-test.js deleted file mode 100644 index 9bc646c0dbbe..000000000000 --- a/private/monorepo-tests/__tests__/no-dependencies-in-root-package-test.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 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. - * - * @flow strict-local - * @format - */ - -import fs from 'fs'; -import path from 'path'; - -const CURRENT_DIR = __dirname; -const PATH_TO_ROOT_PACKAGE_MANIFEST = path.join( - CURRENT_DIR, - '..', - '..', - '..', - 'package.json', -); - -const manifest = JSON.parse( - fs.readFileSync(PATH_TO_ROOT_PACKAGE_MANIFEST).toString(), -); - -describe('@react-native/monorepo root package', () => { - it('expected not to list any dependencies', () => { - expect(manifest).not.toHaveProperty('dependencies'); - }); -}); diff --git a/private/monorepo-tests/__tests__/no-dev-dependencies-in-react-native-package-test.js b/private/monorepo-tests/__tests__/no-dev-dependencies-in-react-native-package-test.js deleted file mode 100644 index 0ce252a19469..000000000000 --- a/private/monorepo-tests/__tests__/no-dev-dependencies-in-react-native-package-test.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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. - * - * @flow strict-local - * @format - */ - -import fs from 'fs'; -import path from 'path'; - -const CURRENT_DIR = __dirname; -const PATH_TO_REACT_NATIVE_PACKAGE_MANIFEST = path.join( - CURRENT_DIR, - '..', - '..', - '..', - 'packages', - 'react-native', - 'package.json', -); - -const manifest = JSON.parse( - fs.readFileSync(PATH_TO_REACT_NATIVE_PACKAGE_MANIFEST).toString(), -); - -describe('react-native package', () => { - it('expected not to list any devDependencies', () => { - expect(manifest).not.toHaveProperty('devDependencies'); - }); -}); diff --git a/private/monorepo-tests/__tests__/no-dev-dependencies-in-tester-package.js b/private/monorepo-tests/__tests__/no-dev-dependencies-in-tester-package.js deleted file mode 100644 index 3a3e48430c13..000000000000 --- a/private/monorepo-tests/__tests__/no-dev-dependencies-in-tester-package.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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. - * - * @flow strict-local - * @format - */ - -import fs from 'fs'; -import path from 'path'; - -const CURRENT_DIR = __dirname; -const PATH_TO_TESTER_PACKAGE_MANIFEST = path.join( - CURRENT_DIR, - '..', - '..', - '..', - 'packages', - 'rn-tester', - 'package.json', -); - -const manifest = JSON.parse( - fs.readFileSync(PATH_TO_TESTER_PACKAGE_MANIFEST).toString(), -); - -describe('@react-native/tester package', () => { - it('expected not to list any devDependencies', () => { - expect(manifest).not.toHaveProperty('devDependencies'); - }); -}); diff --git a/private/monorepo-tests/package.json b/private/monorepo-tests/package.json deleted file mode 100644 index 10725c529541..000000000000 --- a/private/monorepo-tests/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "@react-native/monorepo-tests", - "private": true, - "version": "0.0.0" -} diff --git a/scripts/monorepo-tests/__tests__/check-packages-test.js b/scripts/monorepo-tests/__tests__/check-packages-test.js new file mode 100644 index 000000000000..3fc0611203a1 --- /dev/null +++ b/scripts/monorepo-tests/__tests__/check-packages-test.js @@ -0,0 +1,42 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import {REPO_ROOT} from '../../shared/consts'; +import { + getReactNativePackage, + getWorkspaceRoot, +} from '../../shared/monorepoUtils'; +import {globSync} from 'tinyglobby'; + +describe('package manifests', () => { + test('the workspace root must not declare runtime dependencies', async () => { + const {packageJson} = await getWorkspaceRoot(); + expect(packageJson).not.toHaveProperty('dependencies'); + }); + + test('the react-native package must not declare devDependencies', async () => { + const {packageJson} = await getReactNativePackage(); + expect(packageJson).not.toHaveProperty('devDependencies'); + }); +}); + +describe('package file structure', () => { + test('packages must not contain .npmignore files', () => { + // Publishing must be controlled via the package.json "files" field, which is + // easier to audit and does not silently expand what gets shipped to npm. + const npmignoreFiles = globSync('{packages,private}/**/.npmignore', { + cwd: REPO_ROOT, + dot: true, + ignore: ['**/node_modules/**'], + }); + + expect(npmignoreFiles).toEqual([]); + }); +}); From 9a77d4bf6ebb171987f9dbdc9b219b9883d0b323 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Fri, 10 Jul 2026 11:06:18 -0700 Subject: [PATCH 2/2] Extend check-packages-test to validate additional required fields (#57510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/react/react-native/pull/57510 Extend `check-packages-test.js` to catch two more classes of manifest drift: published packages missing required fields, and packages under `private/` missing the `private` flag. **Motivation** Inspired by https://github.com/react-native-community/template/pull/241 — avoid a missing field blocking a future RN package publish. **Other changes** - To satisfy the new `files` requirement, add an explicit `files` allowlist to the four config packages that lacked one. As a side effect, this saves some `__tests__` files from being distributed. Changelog: [Internal] Differential Revision: D111471314 --- .../eslint-config-react-native/package.json | 1 + .../eslint-plugin-react-native/package.json | 7 +++ packages/eslint-plugin-specs/package.json | 6 +++ packages/typescript-config/package.json | 3 +- .../__tests__/check-packages-test.js | 50 ++++++++++++++++++- scripts/shared/monorepoUtils.js | 7 ++- 6 files changed, 70 insertions(+), 4 deletions(-) diff --git a/packages/eslint-config-react-native/package.json b/packages/eslint-config-react-native/package.json index 937b51e14be1..99fc3a8d2e9c 100644 --- a/packages/eslint-config-react-native/package.json +++ b/packages/eslint-config-react-native/package.json @@ -23,6 +23,7 @@ ".": "./index.js", "./flat": "./flat.js" }, + "files": ["README.md", "flat.js", "index.js", "shared.js"], "dependencies": { "@babel/core": "^7.25.2", "@babel/eslint-parser": "^7.25.1", diff --git a/packages/eslint-plugin-react-native/package.json b/packages/eslint-plugin-react-native/package.json index c655ccbd8849..d6900b4ff4c8 100644 --- a/packages/eslint-plugin-react-native/package.json +++ b/packages/eslint-plugin-react-native/package.json @@ -17,6 +17,13 @@ ], "bugs": "https://github.com/react/react-native/issues", "main": "index.js", + "files": [ + "README.md", + "index.js", + "no-deep-imports.js", + "platform-colors.js", + "utils.js" + ], "devDependencies": { "babel-plugin-syntax-hermes-parser": "0.36.1", "hermes-eslint": "0.36.1" diff --git a/packages/eslint-plugin-specs/package.json b/packages/eslint-plugin-specs/package.json index 60ad0fb96725..882071d9c484 100644 --- a/packages/eslint-plugin-specs/package.json +++ b/packages/eslint-plugin-specs/package.json @@ -19,6 +19,12 @@ ], "bugs": "https://github.com/react/react-native/issues", "main": "index.js", + "files": [ + "README.md", + "index.js", + "react-native-modules.js", + "with-babel-register" + ], "scripts": { "prepack": "node prepack.js", "postpack": "node postpack.js" diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 98c39c04112b..8d938d94b0e1 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -17,5 +17,6 @@ "bugs": "https://github.com/react/react-native/issues", "exports": { ".": "./tsconfig.json" - } + }, + "files": ["README.md", "tsconfig.json", "tsconfig.strict.json"] } diff --git a/scripts/monorepo-tests/__tests__/check-packages-test.js b/scripts/monorepo-tests/__tests__/check-packages-test.js index 3fc0611203a1..93dd3ba58a58 100644 --- a/scripts/monorepo-tests/__tests__/check-packages-test.js +++ b/scripts/monorepo-tests/__tests__/check-packages-test.js @@ -8,11 +8,13 @@ * @format */ -import {REPO_ROOT} from '../../shared/consts'; +import {PRIVATE_DIR, REPO_ROOT} from '../../shared/consts'; import { + getPackages, getReactNativePackage, getWorkspaceRoot, } from '../../shared/monorepoUtils'; +import path from 'path'; import {globSync} from 'tinyglobby'; describe('package manifests', () => { @@ -25,6 +27,52 @@ describe('package manifests', () => { const {packageJson} = await getReactNativePackage(); expect(packageJson).not.toHaveProperty('devDependencies'); }); + + test('published packages must declare required fields', async () => { + const packages = await getPackages({includeReactNative: true}); + const violations: Array = []; + + for (const name of Object.keys(packages)) { + const {packageJson} = packages[name]; + + if (!packageJson.version) { + violations.push(`${name}: missing "version"`); + } + if (packageJson.license == null || packageJson.license === '') { + violations.push(`${name}: missing "license"`); + } + // "repository" is required for npm's trusted publishing / provenance (OIDC) + if ( + packageJson.repository?.url == null || + packageJson.repository.url === '' + ) { + violations.push(`${name}: missing "repository.url"`); + } + if ( + packageJson.repository?.directory == null || + packageJson.repository.directory === '' + ) { + violations.push(`${name}: missing "repository.directory"`); + } + if (packageJson.files == null || packageJson.files.length === 0) { + violations.push(`${name}: missing "files"`); + } + } + + expect(violations).toEqual([]); + }); + + test('packages under private/ must set "private": true', async () => { + const packages = await getPackages({ + includeReactNative: true, + includePrivate: true, + }); + const notPrivate = Object.keys(packages) + .filter(name => packages[name].path.startsWith(PRIVATE_DIR + path.sep)) + .filter(name => packages[name].packageJson.private !== true); + + expect(notPrivate).toEqual([]); + }); }); describe('package file structure', () => { diff --git a/scripts/shared/monorepoUtils.js b/scripts/shared/monorepoUtils.js index eecb81250a01..0e43fa0f8455 100644 --- a/scripts/shared/monorepoUtils.js +++ b/scripts/shared/monorepoUtils.js @@ -19,11 +19,14 @@ const WORKSPACES_CONFIG = '{packages,private}/*'; export type PackageJson = { name: string, version: string, - private?: boolean, dependencies?: Record, devDependencies?: Record, - peerDependencies?: Record, + files?: ReadonlyArray, + license?: string, main?: string, + peerDependencies?: Record, + private?: boolean, + repository?: {type?: string, url?: string, directory?: string, ...}, ... };