diff --git a/.eslintrc.js b/.eslintrc.js index da8717076500fb..f14dc495f236e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,16 @@ module.exports = { 'no-undef': 0, }, }, + { + files: ['package.json'], + parser: 'jsonc-eslint-parser', + }, + { + files: ['package.json'], + rules: { + 'lint/react-native-manifest': 2, + }, + }, { files: ['flow-typed/**/*.js'], rules: { diff --git a/package.json b/package.json index 99ba86cfcda926..ec19e9b585b139 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "update-lock": "npx yarn-deduplicate" }, "workspaces": [ - "packages/*" + "packages/*", + "tools/*" ], "peerDependencies": { "react": "18.2.0" diff --git a/tools/eslint/package.json b/tools/eslint/package.json new file mode 100644 index 00000000000000..8a45d1133bbc15 --- /dev/null +++ b/tools/eslint/package.json @@ -0,0 +1,8 @@ +{ + "name": "@react-native/eslint", + "private": true, + "version": "0.0.0", + "dependencies": { + "jsonc-eslint-parser": "^2.3.0" + } +} diff --git a/tools/eslint/rules/__tests__/react-native-manifest-test.js b/tools/eslint/rules/__tests__/react-native-manifest-test.js new file mode 100644 index 00000000000000..3bff931bac9ecb --- /dev/null +++ b/tools/eslint/rules/__tests__/react-native-manifest-test.js @@ -0,0 +1,91 @@ +/** + * 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 + */ + +'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/tools/eslint/rules/react-native-manifest.js b/tools/eslint/rules/react-native-manifest.js new file mode 100644 index 00000000000000..9fa488f78a624c --- /dev/null +++ b/tools/eslint/rules/react-native-manifest.js @@ -0,0 +1,84 @@ +/** + * 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 + */ + +'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/yarn.lock b/yarn.lock index 5b6555117deb27..95222d7d302834 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5650,6 +5650,11 @@ eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== +eslint-visitor-keys@^3.0.0: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" @@ -5700,7 +5705,7 @@ eslint@^8.17.0, eslint@^8.19.0, eslint@^8.23.1: strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.4.0: +espree@^9.0.0, espree@^9.4.0: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -7727,6 +7732,16 @@ json5@2.2.3, json5@^2.1.0, json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-eslint-parser@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.3.0.tgz#7c2de97d01bff7227cbef2f25d1025d42a36198b" + integrity sha512-9xZPKVYp9DxnM3sd1yAsh/d59iIaswDkai8oTxbursfKYbg/ibjX0IzFt35+VZ8iEW453TVTXztnRvYUQlAfUQ== + dependencies: + acorn "^8.5.0" + eslint-visitor-keys "^3.0.0" + espree "^9.0.0" + semver "^7.3.5" + jsonc-parser@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"