diff --git a/src/main.test.ts b/src/main.test.ts index 2afc7b1..7676b96 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -73,7 +73,15 @@ describe('main', () => { JSON.stringify({ packageManager: 'yarn', engines: { node: 'test' }, - devDependencies: { eslint: '1.1.0' }, + devDependencies: { + eslint: '1.1.0', + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: 'test script', + 'test:watch': 'test watch script', + }, }), ); await writeFile( @@ -84,6 +92,10 @@ describe('main', () => { path.join(repository.directoryPath, '.nvmrc'), 'content for .nvmrc', ); + await writeFile( + path.join(repository.directoryPath, 'jest.config.js'), + 'content for jest.config.js', + ); } const outputLogger = new FakeOutputLogger(); @@ -108,6 +120,8 @@ repo-1 - Does the \`packageManager\` field in \`package.json\` conform? ✅ - Does the \`engines.node\` field in \`package.json\` conform? ✅ - Do the lint-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the jest-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the test-related \`scripts\` in \`package.json\` conform? ✅ - Is \`README.md\` present? ✅ - Does the README conform by recommending the correct Yarn version to install? ✅ - Does the README conform by recommending node install from nodejs.org? ✅ @@ -115,8 +129,9 @@ repo-1 - Does the README conform by recommending the correct Yarn version to install? ✅ - Does the \`src/\` directory exist? ✅ - Is \`.nvmrc\` present, and does it conform? ✅ +- Is \`jest.config.js\` present, and does it conform? ✅ -Results: 12 passed, 0 failed, 12 total +Results: 15 passed, 0 failed, 15 total Elapsed time: 0 ms @@ -128,6 +143,8 @@ repo-2 - Does the \`packageManager\` field in \`package.json\` conform? ✅ - Does the \`engines.node\` field in \`package.json\` conform? ✅ - Do the lint-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the jest-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the test-related \`scripts\` in \`package.json\` conform? ✅ - Is \`README.md\` present? ✅ - Does the README conform by recommending the correct Yarn version to install? ✅ - Does the README conform by recommending node install from nodejs.org? ✅ @@ -135,8 +152,9 @@ repo-2 - Does the README conform by recommending the correct Yarn version to install? ✅ - Does the \`src/\` directory exist? ✅ - Is \`.nvmrc\` present, and does it conform? ✅ +- Is \`jest.config.js\` present, and does it conform? ✅ -Results: 12 passed, 0 failed, 12 total +Results: 15 passed, 0 failed, 15 total Elapsed time: 0 ms `, @@ -195,8 +213,10 @@ repo-1 - \`src/\` does not exist in this project. - Is \`.nvmrc\` present, and does it conform? ❌ - \`.nvmrc\` does not exist in this project. +- Is \`jest.config.js\` present, and does it conform? ❌ + - \`jest.config.js\` does not exist in this project. -Results: 0 passed, 6 failed, 6 total +Results: 0 passed, 7 failed, 7 total Elapsed time: 0 ms @@ -217,8 +237,10 @@ repo-2 - \`src/\` does not exist in this project. - Is \`.nvmrc\` present, and does it conform? ❌ - \`.nvmrc\` does not exist in this project. +- Is \`jest.config.js\` present, and does it conform? ❌ + - \`jest.config.js\` does not exist in this project. -Results: 0 passed, 6 failed, 6 total +Results: 0 passed, 7 failed, 7 total Elapsed time: 0 ms `, @@ -283,8 +305,10 @@ repo-2 - \`src/\` does not exist in this project. - Is \`.nvmrc\` present, and does it conform? ❌ - \`.nvmrc\` does not exist in this project. +- Is \`jest.config.js\` present, and does it conform? ❌ + - \`jest.config.js\` does not exist in this project. -Results: 1 passed, 5 failed, 6 total +Results: 1 passed, 6 failed, 7 total Elapsed time: 0 ms `.trimStart(), @@ -340,7 +364,15 @@ Elapsed time: 0 ms JSON.stringify({ packageManager: 'yarn', engines: { node: 'test' }, - devDependencies: { eslint: '1.1.0' }, + devDependencies: { + eslint: '1.1.0', + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: 'test script', + 'test:watch': 'test watch script', + }, }), ); await writeFile( @@ -351,6 +383,10 @@ Elapsed time: 0 ms path.join(repository.directoryPath, '.nvmrc'), 'content for .nvmrc', ); + await writeFile( + path.join(repository.directoryPath, 'jest.config.js'), + 'content for jest.config.js', + ); } const outputLogger = new FakeOutputLogger(); @@ -375,6 +411,8 @@ repo-1 - Does the \`packageManager\` field in \`package.json\` conform? ✅ - Does the \`engines.node\` field in \`package.json\` conform? ✅ - Do the lint-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the jest-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the test-related \`scripts\` in \`package.json\` conform? ✅ - Is \`README.md\` present? ✅ - Does the README conform by recommending the correct Yarn version to install? ✅ - Does the README conform by recommending node install from nodejs.org? ✅ @@ -382,8 +420,9 @@ repo-1 - Does the README conform by recommending the correct Yarn version to install? ✅ - Does the \`src/\` directory exist? ✅ - Is \`.nvmrc\` present, and does it conform? ✅ +- Is \`jest.config.js\` present, and does it conform? ✅ -Results: 12 passed, 0 failed, 12 total +Results: 15 passed, 0 failed, 15 total Elapsed time: 0 ms @@ -395,6 +434,8 @@ repo-2 - Does the \`packageManager\` field in \`package.json\` conform? ✅ - Does the \`engines.node\` field in \`package.json\` conform? ✅ - Do the lint-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the jest-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the test-related \`scripts\` in \`package.json\` conform? ✅ - Is \`README.md\` present? ✅ - Does the README conform by recommending the correct Yarn version to install? ✅ - Does the README conform by recommending node install from nodejs.org? ✅ @@ -402,8 +443,9 @@ repo-2 - Does the README conform by recommending the correct Yarn version to install? ✅ - Does the \`src/\` directory exist? ✅ - Is \`.nvmrc\` present, and does it conform? ✅ +- Is \`jest.config.js\` present, and does it conform? ✅ -Results: 12 passed, 0 failed, 12 total +Results: 15 passed, 0 failed, 15 total Elapsed time: 0 ms `, @@ -462,8 +504,10 @@ repo-1 - \`src/\` does not exist in this project. - Is \`.nvmrc\` present, and does it conform? ❌ - \`.nvmrc\` does not exist in this project. +- Is \`jest.config.js\` present, and does it conform? ❌ + - \`jest.config.js\` does not exist in this project. -Results: 0 passed, 6 failed, 6 total +Results: 0 passed, 7 failed, 7 total Elapsed time: 0 ms @@ -484,8 +528,10 @@ repo-2 - \`src/\` does not exist in this project. - Is \`.nvmrc\` present, and does it conform? ❌ - \`.nvmrc\` does not exist in this project. +- Is \`jest.config.js\` present, and does it conform? ❌ + - \`jest.config.js\` does not exist in this project. -Results: 0 passed, 6 failed, 6 total +Results: 0 passed, 7 failed, 7 total Elapsed time: 0 ms `, @@ -549,8 +595,10 @@ repo-2 - \`src/\` does not exist in this project. - Is \`.nvmrc\` present, and does it conform? ❌ - \`.nvmrc\` does not exist in this project. +- Is \`jest.config.js\` present, and does it conform? ❌ + - \`jest.config.js\` does not exist in this project. -Results: 1 passed, 5 failed, 6 total +Results: 1 passed, 6 failed, 7 total Elapsed time: 0 ms `, diff --git a/src/rules/index.ts b/src/rules/index.ts index ced17c7..c31fb49 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1,10 +1,13 @@ import allYarnModernFilesConform from './all-yarn-modern-files-conform'; import classicYarnConfigFileAbsent from './classic-yarn-config-file-absent'; import packageEnginesNodeFieldConforms from './package-engines-node-field-conforms'; +import packageJestDependenciesConform from './package-jest-dependencies-conform'; import packageLintDependenciesConform from './package-lint-dependencies-conform'; import packagePackageManagerFieldConforms from './package-package-manager-field-conforms'; +import packageTestScriptsConform from './package-test-scripts-conform'; import readmeListsCorrectYarnVersion from './readme-lists-correct-yarn-version'; import readmeListsNodejsWebsite from './readme-recommends-node-install'; +import requireJestConfig from './require-jest-config'; import requireNvmrc from './require-nvmrc'; import requireReadme from './require-readme'; import requireSourceDirectory from './require-source-directory'; @@ -22,4 +25,7 @@ export const rules = [ packageEnginesNodeFieldConforms, readmeListsNodejsWebsite, packageLintDependenciesConform, + packageJestDependenciesConform, + requireJestConfig, + packageTestScriptsConform, ] as const; diff --git a/src/rules/package-engines-node-field-conforms.test.ts b/src/rules/package-engines-node-field-conforms.test.ts index 412c2bf..03f7daa 100644 --- a/src/rules/package-engines-node-field-conforms.test.ts +++ b/src/rules/package-engines-node-field-conforms.test.ts @@ -18,6 +18,7 @@ describe('Rule: package-engines-node-field-conforms', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: 'jest && jest-it-up' }, }), ); const project = buildMetaMaskRepository({ @@ -30,6 +31,7 @@ describe('Rule: package-engines-node-field-conforms', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: 'jest && jest-it-up' }, }), ); @@ -58,6 +60,7 @@ describe('Rule: package-engines-node-field-conforms', () => { packageManager: 'a', engines: { node: 'test1' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: 'jest && jest-it-up' }, }), ); const project = buildMetaMaskRepository({ @@ -70,6 +73,7 @@ describe('Rule: package-engines-node-field-conforms', () => { packageManager: 'a', engines: { node: 'test2' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: 'jest && jest-it-up' }, }), ); diff --git a/src/rules/package-jest-dependencies-conform.test.ts b/src/rules/package-jest-dependencies-conform.test.ts new file mode 100644 index 0000000..128bae3 --- /dev/null +++ b/src/rules/package-jest-dependencies-conform.test.ts @@ -0,0 +1,223 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageJestDependenciesConform from './package-jest-dependencies-conform'; +import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-jest-dependencies-conform', () => { + it("passes if the jest related dependencies in the project's package.json match the ones in the template's package.json", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: '', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: '', + }, + }), + ); + + const result = await packageJestDependenciesConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it("fails if the version of a jest related dependency in the project's package.json does not match the version of the same dependency in the template's package.json", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.1.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: '', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: '', + }, + }), + ); + + const result = await packageJestDependenciesConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { message: '`jest` is "1.0.0", when it should be "1.1.0".' }, + ], + }); + }); + }); + + it("fails if the jest related dependency exists in the template's package.json, but not in the project's package.json", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.1.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: '', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + 'jest-it-up': '1.0.0', + }, + scripts: { + test: '', + }, + }), + ); + + const result = await packageJestDependenciesConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + '`package.json` should list `"jest": "1.1.0"` in `devDependencies`, but does not.', + }, + ], + }); + }); + }); + + it("throws error if required jest related dependency in the template's package.json does not exist", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + 'jest-it-up': '1.0.0', + }, + scripts: { + test: '', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: '', + }, + }), + ); + + await expect( + packageJestDependenciesConform.execute({ + template, + project, + pass, + fail, + }), + ).rejects.toThrow( + 'Could not find "jest" in `devDependencies` of template\'s package.json. This is not the fault of the project, but is rather a bug in a rule.', + ); + }); + }); +}); diff --git a/src/rules/package-jest-dependencies-conform.ts b/src/rules/package-jest-dependencies-conform.ts new file mode 100644 index 0000000..8242b18 --- /dev/null +++ b/src/rules/package-jest-dependencies-conform.ts @@ -0,0 +1,69 @@ +import { buildRule } from './build-rule'; +import { PackageManifestSchema, RuleName } from './types'; +import type { RuleExecutionFailure } from '../execute-rules'; + +export default buildRule({ + name: RuleName.PackageJestDependenciesConform, + description: + 'Do the jest-related `devDependencies` in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async ({ project, template, pass, fail }) => { + const entryPath = 'package.json'; + + const templateManifest = await template.fs.readJsonFileAs( + entryPath, + PackageManifestSchema, + ); + + const projectManifest = await project.fs.readJsonFileAs( + entryPath, + PackageManifestSchema, + ); + + const failures: RuleExecutionFailure[] = await jestConform( + templateManifest.devDependencies, + projectManifest.devDependencies, + ); + + return failures.length === 0 ? pass() : fail(failures); + }, +}); + +/** + * Validates whether target project has all the required jest packages with versions matching with template project. + * + * @param templateDependencies - The record of jest package name and version from template. + * @param projectDependencies - The record of jest package name and version from project. + */ +async function jestConform( + templateDependencies: Record, + projectDependencies: Record, +): Promise { + const jestPackagesRequired = ['jest', 'jest-it-up']; + const failures: RuleExecutionFailure[] = []; + for (const jestPackage of jestPackagesRequired) { + const templatePackageVersion = templateDependencies[jestPackage]; + if (!templatePackageVersion) { + throw new Error( + `Could not find "${jestPackage}" in \`devDependencies\` of template's package.json. This is not the fault of the project, but is rather a bug in a rule.`, + ); + } + + const projectPackageVersion = projectDependencies[jestPackage]; + if (!projectPackageVersion) { + failures.push({ + message: `\`package.json\` should list \`"${jestPackage}": "${templatePackageVersion}"\` in \`devDependencies\`, but does not.`, + }); + + continue; + } + + if (projectPackageVersion !== templatePackageVersion) { + failures.push({ + message: `\`${jestPackage}\` is "${projectPackageVersion}", when it should be "${templatePackageVersion}".`, + }); + } + } + + return failures; +} diff --git a/src/rules/package-lint-dependencies-conform.test.ts b/src/rules/package-lint-dependencies-conform.test.ts index 14394e1..ee800bf 100644 --- a/src/rules/package-lint-dependencies-conform.test.ts +++ b/src/rules/package-lint-dependencies-conform.test.ts @@ -27,6 +27,7 @@ describe('Rule: package-lint-dependencies-conform', () => { 'prettier-plugin-foo': '1.0.0', 'prettier-config-foo': '1.0.0', }, + scripts: { test: '' }, }), ); const project = buildMetaMaskRepository({ @@ -48,6 +49,7 @@ describe('Rule: package-lint-dependencies-conform', () => { 'prettier-plugin-foo': '1.0.0', 'prettier-config-foo': '1.0.0', }, + scripts: { test: '' }, }), ); @@ -76,6 +78,7 @@ describe('Rule: package-lint-dependencies-conform', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { eslint: '1.1.0' }, + scripts: { test: '' }, }), ); const project = buildMetaMaskRepository({ @@ -88,6 +91,7 @@ describe('Rule: package-lint-dependencies-conform', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: '' }, }), ); @@ -119,6 +123,7 @@ describe('Rule: package-lint-dependencies-conform', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { eslint: '1.1.0' }, + scripts: { test: '' }, }), ); const project = buildMetaMaskRepository({ @@ -131,6 +136,7 @@ describe('Rule: package-lint-dependencies-conform', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { testlint: '1.0.0' }, + scripts: { test: '' }, }), ); @@ -167,6 +173,7 @@ describe('Rule: package-lint-dependencies-conform', () => { devDependencies: { '@metamask/test-config-foo': '1.0.0', }, + scripts: { test: '' }, }), ); const project = buildMetaMaskRepository({ @@ -188,6 +195,7 @@ describe('Rule: package-lint-dependencies-conform', () => { 'prettier-plugin-foo': '1.0.0', 'prettier-config-foo': '1.0.0', }, + scripts: { test: '' }, }), ); diff --git a/src/rules/package-package-manager-field-conforms.test.ts b/src/rules/package-package-manager-field-conforms.test.ts index 7631c7f..fce2622 100644 --- a/src/rules/package-package-manager-field-conforms.test.ts +++ b/src/rules/package-package-manager-field-conforms.test.ts @@ -18,6 +18,7 @@ describe('Rule: package-manager-field-conforms', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: '' }, }), ); const project = buildMetaMaskRepository({ @@ -30,6 +31,7 @@ describe('Rule: package-manager-field-conforms', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: '' }, }), ); @@ -58,6 +60,7 @@ describe('Rule: package-manager-field-conforms', () => { packageManager: 'a', engines: { node: 'test' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: '' }, }), ); const project = buildMetaMaskRepository({ @@ -70,6 +73,7 @@ describe('Rule: package-manager-field-conforms', () => { packageManager: 'b', engines: { node: 'test' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: '' }, }), ); diff --git a/src/rules/package-test-scripts-conform.test.ts b/src/rules/package-test-scripts-conform.test.ts new file mode 100644 index 0000000..6197b0c --- /dev/null +++ b/src/rules/package-test-scripts-conform.test.ts @@ -0,0 +1,218 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageTestScriptsConform from './package-test-scripts-conform'; +import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-test-scripts-conform', () => { + it("passes if the test related scripts in the project's package.json match the ones in the template's package.json", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: 'test script', + 'test:watch': 'test watch script', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: 'test script', + 'test:watch': 'test watch script', + }, + }), + ); + + const result = await packageTestScriptsConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it("fails if a test related script in the project's package.json does not match the same one in the template's package.json", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { jest: '1.1.0' }, + scripts: { + test: 'test script', + 'test:watch': 'test watch script', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { jest: '1.0.0' }, + scripts: { + test: 'test', + 'test:watch': 'test watch script', + }, + }), + ); + + const result = await packageTestScriptsConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { message: '`test` is "test", when it should be "test script".' }, + ], + }); + }); + }); + + it("fails if a test related script exists in the template's package.json, but not in the project's package.json", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { jest: '1.1.0' }, + scripts: { + test: 'test script', + 'test:watch': 'test watch script', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { test: '1.0.0' }, + scripts: { + 'test:watch': 'test watch script', + }, + }), + ); + + const result = await packageTestScriptsConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + '`package.json` should list `"test": "test script"` in `scripts`, but does not.', + }, + ], + }); + }); + }); + + it("throws error if there are no test related scripts in the template's package.json", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + foo: '1.0.0', + }, + scripts: { + 'test:watch': 'test watch script', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + JSON.stringify({ + packageManager: 'a', + engines: { node: 'test' }, + devDependencies: { + jest: '1.0.0', + 'jest-it-up': '1.0.0', + }, + scripts: { + test: 'test scripts', + 'test:watch': 'test watch scripts', + }, + }), + ); + + await expect( + packageTestScriptsConform.execute({ + template, + project, + pass, + fail, + }), + ).rejects.toThrow( + 'Could not find "test" in `scripts` of template\'s package.json. This is not the fault of the project, but is rather a bug in a rule.', + ); + }); + }); +}); diff --git a/src/rules/package-test-scripts-conform.ts b/src/rules/package-test-scripts-conform.ts new file mode 100644 index 0000000..9d154f7 --- /dev/null +++ b/src/rules/package-test-scripts-conform.ts @@ -0,0 +1,68 @@ +import { buildRule } from './build-rule'; +import { PackageManifestSchema, RuleName } from './types'; +import type { RuleExecutionFailure } from '../execute-rules'; + +export default buildRule({ + name: RuleName.PackageTestScriptsConform, + description: 'Do the test-related `scripts` in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async ({ project, template, pass, fail }) => { + const entryPath = 'package.json'; + + const templateManifest = await template.fs.readJsonFileAs( + entryPath, + PackageManifestSchema, + ); + + const projectManifest = await project.fs.readJsonFileAs( + entryPath, + PackageManifestSchema, + ); + + const failures: RuleExecutionFailure[] = await testScriptsConform( + templateManifest.scripts, + projectManifest.scripts, + ); + + return failures.length === 0 ? pass() : fail(failures); + }, +}); + +/** + * Validates whether target project has all the required test scripts matching with template project. + * + * @param templateScripts - The record of key and value from template scripts. + * @param projectScripts - The record of key and value from project scripts. + */ +async function testScriptsConform( + templateScripts: Record, + projectScripts: Record, +): Promise { + const testScriptsRequired = ['test', 'test:watch']; + const failures: RuleExecutionFailure[] = []; + for (const testScriptKey of testScriptsRequired) { + const templateScript = templateScripts[testScriptKey]; + if (!templateScript) { + throw new Error( + `Could not find "${testScriptKey}" in \`scripts\` of template's package.json. This is not the fault of the project, but is rather a bug in a rule.`, + ); + } + + const projectScript = projectScripts[testScriptKey]; + if (!projectScript) { + failures.push({ + message: `\`package.json\` should list \`"${testScriptKey}": "${templateScript}"\` in \`scripts\`, but does not.`, + }); + + continue; + } + + if (projectScript !== templateScript) { + failures.push({ + message: `\`${testScriptKey}\` is "${projectScript}", when it should be "${templateScript}".`, + }); + } + } + + return failures; +} diff --git a/src/rules/require-jest-config.ts b/src/rules/require-jest-config.ts new file mode 100644 index 0000000..4d8f1e1 --- /dev/null +++ b/src/rules/require-jest-config.ts @@ -0,0 +1,12 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { fileConforms } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.RequireJestConfig, + description: 'Is `jest.config.js` present, and does it conform?', + dependencies: [], + execute: async (ruleExecutionArguments) => { + return await fileConforms('jest.config.js', ruleExecutionArguments); + }, +}); diff --git a/src/rules/require-valid-package-manifest.test.ts b/src/rules/require-valid-package-manifest.test.ts index 44d5cf8..bffa2cf 100644 --- a/src/rules/require-valid-package-manifest.test.ts +++ b/src/rules/require-valid-package-manifest.test.ts @@ -18,6 +18,7 @@ describe('Rule: require-package-manifest', () => { packageManager: 'foo', engines: { node: 'test' }, devDependencies: { eslint: '1.0.0' }, + scripts: { test: 'test script' }, }), ); @@ -82,7 +83,7 @@ describe('Rule: require-package-manifest', () => { failures: [ { message: - 'Invalid `package.json`: Missing `packageManager`; Missing `engines`; Missing `devDependencies`.', + 'Invalid `package.json`: Missing `packageManager`; Missing `engines`; Missing `scripts`; Missing `devDependencies`.', }, ], }); diff --git a/src/rules/types.ts b/src/rules/types.ts index 0e7c34c..f3a8e68 100644 --- a/src/rules/types.ts +++ b/src/rules/types.ts @@ -15,6 +15,9 @@ export enum RuleName { PackageEnginesNodeFieldConforms = 'package-engines-node-field-conforms', ReadmeRecommendsNodeInstall = 'readme-recommends-node-install', PackageLintDependenciesConform = 'package-lint-dependencies-conform', + PackageJestDependenciesConform = 'package-jest-dependencies-conform', + RequireJestConfig = 'require-jest-config', + PackageTestScriptsConform = 'package-test-scripts-conform', } export const PackageManifestSchema = type({ @@ -22,5 +25,6 @@ export const PackageManifestSchema = type({ engines: type({ node: string(), }), + scripts: record(string(), string()), devDependencies: record(string(), string()), });