diff --git a/package.json b/package.json index 3031b0f..87ff88c 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "chalk": "^4.1.2", "dependency-graph": "^0.11.0", "execa": "^5.1.1", + "lodash": "^4.17.21", "superstruct": "^1.0.3", "yargs": "^17.7.2" }, @@ -68,6 +69,7 @@ "@swc/cli": "^0.1.62", "@swc/core": "^1.3.66", "@types/jest": "^28.1.6", + "@types/lodash": "^4.14.202", "@types/node": "^16", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", diff --git a/src/main.test.ts b/src/main.test.ts index 7676b96..2e8ed28 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -10,7 +10,11 @@ import stripAnsi from 'strip-ansi'; import { main } from './main'; import { FakeOutputLogger } from '../tests/fake-output-logger'; import type { PrimaryExecaFunction } from '../tests/helpers'; -import { fakeDateOnly, withinSandbox } from '../tests/helpers'; +import { + buildPackageManifestMock, + fakeDateOnly, + withinSandbox, +} from '../tests/helpers'; import { setupToolWithMockRepositories } from '../tests/setup-tool-with-mock-repositories'; jest.mock('execa'); @@ -70,17 +74,21 @@ describe('main', () => { ); await writeFile( path.join(repository.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'yarn', - engines: { node: 'test' }, + buildPackageManifestMock({ devDependencies: { - eslint: '1.1.0', + test: '1.0.0', jest: '1.0.0', 'jest-it-up': '1.0.0', + '@types/node': '1.0.0', + 'ts-node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', }, scripts: { test: 'test script', 'test:watch': 'test watch script', + build: 'test build', + 'build:types': 'test build types', }, }), ); @@ -96,6 +104,18 @@ describe('main', () => { path.join(repository.directoryPath, 'jest.config.js'), 'content for jest.config.js', ); + await writeFile( + path.join(repository.directoryPath, 'tsconfig.json'), + 'content for tsconfig.json', + ); + await writeFile( + path.join(repository.directoryPath, 'tsconfig.build.json'), + 'content for tsconfig.build.json', + ); + await writeFile( + path.join(repository.directoryPath, 'tsup.config.ts'), + 'content for tsup.config.ts', + ); } const outputLogger = new FakeOutputLogger(); @@ -122,6 +142,14 @@ repo-1 - 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? ✅ + - Do the typescript-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the typescript-related \`scripts\` in \`package.json\` conform? ✅ + - Does the \`exports\` field in \`package.json\` conform? ✅ + - Does the \`main\` field in \`package.json\` conform? ✅ + - Does the \`module\` field in \`package.json\` conform? ✅ + - Does the \`types\` field in \`package.json\` conform? ✅ + - Does the \`files\` field in \`package.json\` conform? ✅ + - Does LavaMoat allow scripts for \`tsup>esbuild\`? ✅ - 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? ✅ @@ -130,8 +158,11 @@ repo-1 - Does the \`src/\` directory exist? ✅ - Is \`.nvmrc\` present, and does it conform? ✅ - Is \`jest.config.js\` present, and does it conform? ✅ +- Is \`tsconfig.json\` present, and does it conform? ✅ +- Is \`tsconfig.build.json\` present, and does it conform? ✅ +- Is \`tsup.config.ts\` present, and does it conform? ✅ -Results: 15 passed, 0 failed, 15 total +Results: 26 passed, 0 failed, 26 total Elapsed time: 0 ms @@ -145,6 +176,14 @@ repo-2 - 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? ✅ + - Do the typescript-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the typescript-related \`scripts\` in \`package.json\` conform? ✅ + - Does the \`exports\` field in \`package.json\` conform? ✅ + - Does the \`main\` field in \`package.json\` conform? ✅ + - Does the \`module\` field in \`package.json\` conform? ✅ + - Does the \`types\` field in \`package.json\` conform? ✅ + - Does the \`files\` field in \`package.json\` conform? ✅ + - Does LavaMoat allow scripts for \`tsup>esbuild\`? ✅ - 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? ✅ @@ -153,8 +192,11 @@ repo-2 - Does the \`src/\` directory exist? ✅ - Is \`.nvmrc\` present, and does it conform? ✅ - Is \`jest.config.js\` present, and does it conform? ✅ +- Is \`tsconfig.json\` present, and does it conform? ✅ +- Is \`tsconfig.build.json\` present, and does it conform? ✅ +- Is \`tsup.config.ts\` present, and does it conform? ✅ -Results: 15 passed, 0 failed, 15 total +Results: 26 passed, 0 failed, 26 total Elapsed time: 0 ms `, @@ -215,8 +257,14 @@ repo-1 - \`.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, 7 failed, 7 total +- Is \`tsconfig.json\` present, and does it conform? ❌ + - \`tsconfig.json\` does not exist in this project. +- Is \`tsconfig.build.json\` present, and does it conform? ❌ + - \`tsconfig.build.json\` does not exist in this project. +- Is \`tsup.config.ts\` present, and does it conform? ❌ + - \`tsup.config.ts\` does not exist in this project. + +Results: 0 passed, 10 failed, 10 total Elapsed time: 0 ms @@ -239,8 +287,14 @@ repo-2 - \`.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, 7 failed, 7 total +- Is \`tsconfig.json\` present, and does it conform? ❌ + - \`tsconfig.json\` does not exist in this project. +- Is \`tsconfig.build.json\` present, and does it conform? ❌ + - \`tsconfig.build.json\` does not exist in this project. +- Is \`tsup.config.ts\` present, and does it conform? ❌ + - \`tsup.config.ts\` does not exist in this project. + +Results: 0 passed, 10 failed, 10 total Elapsed time: 0 ms `, @@ -307,8 +361,14 @@ repo-2 - \`.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, 6 failed, 7 total +- Is \`tsconfig.json\` present, and does it conform? ❌ + - \`tsconfig.json\` does not exist in this project. +- Is \`tsconfig.build.json\` present, and does it conform? ❌ + - \`tsconfig.build.json\` does not exist in this project. +- Is \`tsup.config.ts\` present, and does it conform? ❌ + - \`tsup.config.ts\` does not exist in this project. + +Results: 1 passed, 9 failed, 10 total Elapsed time: 0 ms `.trimStart(), @@ -361,17 +421,21 @@ Elapsed time: 0 ms ); await writeFile( path.join(repository.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'yarn', - engines: { node: 'test' }, + buildPackageManifestMock({ devDependencies: { - eslint: '1.1.0', + test: '1.0.0', jest: '1.0.0', 'jest-it-up': '1.0.0', + '@types/node': '1.0.0', + 'ts-node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', }, scripts: { test: 'test script', 'test:watch': 'test watch script', + build: 'test build', + 'build:types': 'test build types', }, }), ); @@ -387,6 +451,18 @@ Elapsed time: 0 ms path.join(repository.directoryPath, 'jest.config.js'), 'content for jest.config.js', ); + await writeFile( + path.join(repository.directoryPath, 'tsconfig.json'), + 'content for tsconfig.json', + ); + await writeFile( + path.join(repository.directoryPath, 'tsconfig.build.json'), + 'content for tsconfig.build.json', + ); + await writeFile( + path.join(repository.directoryPath, 'tsup.config.ts'), + 'content for tsup.config.ts', + ); } const outputLogger = new FakeOutputLogger(); @@ -413,6 +489,14 @@ repo-1 - 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? ✅ + - Do the typescript-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the typescript-related \`scripts\` in \`package.json\` conform? ✅ + - Does the \`exports\` field in \`package.json\` conform? ✅ + - Does the \`main\` field in \`package.json\` conform? ✅ + - Does the \`module\` field in \`package.json\` conform? ✅ + - Does the \`types\` field in \`package.json\` conform? ✅ + - Does the \`files\` field in \`package.json\` conform? ✅ + - Does LavaMoat allow scripts for \`tsup>esbuild\`? ✅ - 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? ✅ @@ -421,8 +505,11 @@ repo-1 - Does the \`src/\` directory exist? ✅ - Is \`.nvmrc\` present, and does it conform? ✅ - Is \`jest.config.js\` present, and does it conform? ✅ +- Is \`tsconfig.json\` present, and does it conform? ✅ +- Is \`tsconfig.build.json\` present, and does it conform? ✅ +- Is \`tsup.config.ts\` present, and does it conform? ✅ -Results: 15 passed, 0 failed, 15 total +Results: 26 passed, 0 failed, 26 total Elapsed time: 0 ms @@ -436,6 +523,14 @@ repo-2 - 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? ✅ + - Do the typescript-related \`devDependencies\` in \`package.json\` conform? ✅ + - Do the typescript-related \`scripts\` in \`package.json\` conform? ✅ + - Does the \`exports\` field in \`package.json\` conform? ✅ + - Does the \`main\` field in \`package.json\` conform? ✅ + - Does the \`module\` field in \`package.json\` conform? ✅ + - Does the \`types\` field in \`package.json\` conform? ✅ + - Does the \`files\` field in \`package.json\` conform? ✅ + - Does LavaMoat allow scripts for \`tsup>esbuild\`? ✅ - 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? ✅ @@ -444,8 +539,11 @@ repo-2 - Does the \`src/\` directory exist? ✅ - Is \`.nvmrc\` present, and does it conform? ✅ - Is \`jest.config.js\` present, and does it conform? ✅ +- Is \`tsconfig.json\` present, and does it conform? ✅ +- Is \`tsconfig.build.json\` present, and does it conform? ✅ +- Is \`tsup.config.ts\` present, and does it conform? ✅ -Results: 15 passed, 0 failed, 15 total +Results: 26 passed, 0 failed, 26 total Elapsed time: 0 ms `, @@ -506,8 +604,14 @@ repo-1 - \`.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, 7 failed, 7 total +- Is \`tsconfig.json\` present, and does it conform? ❌ + - \`tsconfig.json\` does not exist in this project. +- Is \`tsconfig.build.json\` present, and does it conform? ❌ + - \`tsconfig.build.json\` does not exist in this project. +- Is \`tsup.config.ts\` present, and does it conform? ❌ + - \`tsup.config.ts\` does not exist in this project. + +Results: 0 passed, 10 failed, 10 total Elapsed time: 0 ms @@ -530,8 +634,14 @@ repo-2 - \`.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, 7 failed, 7 total +- Is \`tsconfig.json\` present, and does it conform? ❌ + - \`tsconfig.json\` does not exist in this project. +- Is \`tsconfig.build.json\` present, and does it conform? ❌ + - \`tsconfig.build.json\` does not exist in this project. +- Is \`tsup.config.ts\` present, and does it conform? ❌ + - \`tsup.config.ts\` does not exist in this project. + +Results: 0 passed, 10 failed, 10 total Elapsed time: 0 ms `, @@ -597,8 +707,14 @@ repo-2 - \`.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, 6 failed, 7 total +- Is \`tsconfig.json\` present, and does it conform? ❌ + - \`tsconfig.json\` does not exist in this project. +- Is \`tsconfig.build.json\` present, and does it conform? ❌ + - \`tsconfig.build.json\` does not exist in this project. +- Is \`tsup.config.ts\` present, and does it conform? ❌ + - \`tsup.config.ts\` does not exist in this project. + +Results: 1 passed, 9 failed, 10 total Elapsed time: 0 ms `, diff --git a/src/rule-helpers.test.ts b/src/rule-helpers.test.ts index 03b007f..a1757c1 100644 --- a/src/rule-helpers.test.ts +++ b/src/rule-helpers.test.ts @@ -6,14 +6,20 @@ import path from 'path'; import { combineRuleExecutionResults, + dataConform, directoryAndContentsConform, directoryExists, fail, fileConforms, fileExists, + packageManifestPropertiesConform, pass, } from './rule-helpers'; -import { buildMetaMaskRepository, withinSandbox } from '../tests/helpers'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../tests/helpers'; describe('pass', () => { it('returns a result that represents a passing rule', () => { @@ -569,3 +575,397 @@ describe('directoryAndContentsConform', () => { }); }); }); + +describe('packageManifestPropertiesConform', () => { + it('passes if the project and template have the same properties at the first level of the package manifest and their values match', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock(), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock(), + ); + const result = await packageManifestPropertiesConform( + ['main', 'module'], + { + template, + project, + pass, + fail, + }, + ); + + expect(result).toStrictEqual({ passed: true }); + }); + }); + + it('passes if the project and template have the same property at a deeper level of the package manifest and its value matches', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { test: '1.0.0', 'test-pack': '1.0.0' }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { test: '1.0.0', 'test-pack': '1.0.0' }, + }), + ); + const result = await packageManifestPropertiesConform( + ["devDependencies.['test']", "devDependencies.['test-pack']"], + { + template, + project, + pass, + fail, + }, + ); + + expect(result).toStrictEqual({ passed: true }); + }); + }); + + it('fails if the project and template have the same properties, but one or more of their values do not match', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { test: '1.0.0', 'test-pack': '1.0.0' }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { test: '0.0.1', 'test-pack': '1.0.0' }, + }), + ); + const result = await packageManifestPropertiesConform( + ["devDependencies.['test']", "devDependencies.['test-pack']"], + { + template, + project, + pass, + fail, + }, + ); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`devDependencies.['test']` is '0.0.1', when it should be '1.0.0'.", + }, + ], + }); + }); + }); + + it('fails if the template has a property that the project does not have', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + test: '1.0.0', + 'new-pack': '1.0.0', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock(), + ); + + const result = await packageManifestPropertiesConform( + ["devDependencies.['new-pack']"], + { + template, + project, + pass, + fail, + }, + ); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`package.json` should list `'devDependencies.['new-pack']': '1.0.0'`, but does not.", + }, + ], + }); + }); + }); + + it("throws error if a property does not exist in the template's package manifest", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock(), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock(), + ); + + await expect( + packageManifestPropertiesConform(["devDependencies.['new-pack']"], { + template, + project, + pass, + fail, + }), + ).rejects.toThrow( + "Could not find `devDependencies.['new-pack']` in reference `package.json`. This is not the fault of the target `package.json`, but is rather a bug in a rule.", + ); + }); + }); +}); + +describe('dataConform', () => { + it('passes if the reference and target have the same properties at the first level and their values match', () => { + const referenceObject = { + firstLevel: 'test', + }; + const targetObject = { + firstLevel: 'test', + }; + const result = dataConform( + referenceObject, + targetObject, + 'firstLevel', + 'test.json', + ); + + expect(result).toStrictEqual({ passed: true }); + }); + it('passes if the reference and target have the same property at a deeper level and its value matches', () => { + const referenceObject = { + firstLevel: 'test', + deeperLevel: { + test: 'test', + }, + }; + const targetObject = { + firstLevel: 'test', + deeperLevel: { + test: 'test', + }, + }; + const result = dataConform( + referenceObject, + targetObject, + "deeperLevel.['test']", + 'test.json', + ); + + expect(result).toStrictEqual({ passed: true }); + }); + it('passes if the target has same properties as reference with additional properties', () => { + const referenceObject = { + firstLevel: 'test', + deeperLevel: { + test: 'test', + }, + }; + const targetObject = { + firstLevel: 'test', + deeperLevel: { + test: 'test', + 'new-pack': 'new pack', + }, + }; + const result = dataConform( + referenceObject, + targetObject, + 'deeperLevel', + 'test.json', + ); + + expect(result).toStrictEqual({ passed: true }); + }); + it('fails if the reference and target does not match at the first level', () => { + const referenceObject = { + firstLevel: 'first level', + deeperLevel: { + test: 'test', + }, + }; + const targetObject = { + firstLevel: 'test', + deeperLevel: { + test: 'test', + }, + }; + + const result = dataConform( + referenceObject, + targetObject, + 'firstLevel', + 'test.json', + ); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: "`firstLevel` is 'test', when it should be 'first level'.", + }, + ], + }); + }); + + it('fails if the reference and target does not match at the deeper level', () => { + const referenceObject = { + firstLevel: 'first level', + deeperLevel: { + test: 'deeper level', + }, + }; + const targetObject = { + firstLevel: 'test', + deeperLevel: { + test: 'test', + }, + }; + + const result = dataConform( + referenceObject, + targetObject, + "deeperLevel.['test']", + 'test.json', + ); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`deeperLevel.['test']` is 'test', when it should be 'deeper level'.", + }, + ], + }); + }); + it('fails if the target is null', () => { + const referenceObject = { + firstLevel: 'first level', + deeperLevel: { + test: 'test', + }, + }; + + const result = dataConform( + referenceObject, + null, + 'firstLevel', + 'test.json', + ); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`test.json` should list `'firstLevel': 'first level'`, but does not.", + }, + ], + }); + }); + + it('fails if the reference has a property at the first level that the target does not have', () => { + const referenceObject = { + firstLevel: 'first level', + deeperLevel: { + test: 'deeper level', + }, + }; + const targetObject = { + deeperLevel: { + test: 'test', + }, + }; + + const result = dataConform( + referenceObject, + targetObject, + 'firstLevel', + 'test.json', + ); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`test.json` should list `'firstLevel': 'first level'`, but does not.", + }, + ], + }); + }); + it('throws error if a propertyPath does not exist in the reference object', () => { + const referenceObject = { + deeperLevel: { + test: 'deeper level', + }, + }; + const targetObject = { + firstLevel: 'first level', + deeperLevel: { + test: 'deeper level', + }, + }; + + expect(() => + dataConform(referenceObject, targetObject, 'firstLevel', 'test.json'), + ).toThrow( + 'Could not find `firstLevel` in reference `test.json`. This is not the fault of the target `test.json`, but is rather a bug in a rule.', + ); + }); +}); diff --git a/src/rule-helpers.ts b/src/rule-helpers.ts index 6fc5ba7..3e80552 100644 --- a/src/rule-helpers.ts +++ b/src/rule-helpers.ts @@ -1,9 +1,14 @@ +import { isEqual, get, has, isMatch, isObject } from 'lodash'; +import { inspect } from 'util'; + import type { SuccessfulPartialRuleExecutionResult, FailedPartialRuleExecutionResult, PartialRuleExecutionResult, RuleExecutionArguments, + RuleExecutionFailure, } from './execute-rules'; +import { PackageManifestSchema } from './rules/types'; /** * A utility for a rule which is intended to end its execution by marking it as @@ -188,3 +193,85 @@ export async function directoryAndContentsConform( ); return combineRuleExecutionResults(fileConformsResults); } + +/** + * Verifies whether project has the required property name/s and with it's value equivalent to the same in template project. + * + * @param propertyPaths - The array of property names to be verified. + * @param ruleExecutionArguments - Rule execution arguments. + */ +export async function packageManifestPropertiesConform( + propertyPaths: string[], + ruleExecutionArguments: RuleExecutionArguments, +): Promise { + const { template, project } = ruleExecutionArguments; + const entryPath = 'package.json'; + const templateManifest = await template.fs.readJsonFileAs( + entryPath, + PackageManifestSchema, + ); + + const projectManifest = await project.fs.readJsonFileAs( + entryPath, + PackageManifestSchema, + ); + + const conformsResults = propertyPaths.map((propertyPath) => { + return dataConform( + templateManifest, + projectManifest, + propertyPath, + entryPath, + ); + }); + return combineRuleExecutionResults(conformsResults); +} + +/** + * Performs a deep comparison between template data and project data to determine if they are equivalent. + * In case of equals, it returns undefined, otherwise failure message. + * + * @param referenceObject - Reference object. + * @param targetObject - The object to be compared with reference object. + * @param propertyPath - Path of the property. + * @param entryPath - The path to the file from which schema is prepared. + * @returns PartialRuleExecutionResult. + */ +export function dataConform( + referenceObject: Schema, + targetObject: Schema, + propertyPath: string, + entryPath: string, +): PartialRuleExecutionResult { + if (!has(referenceObject, propertyPath)) { + throw new Error( + `Could not find \`${propertyPath}\` in reference \`${entryPath}\`. This is not the fault of the target \`${entryPath}\`, but is rather a bug in a rule.`, + ); + } + + const referenceValue = get(referenceObject, propertyPath); + let failure: RuleExecutionFailure | undefined; + if (has(targetObject, propertyPath)) { + const targetValue = get(targetObject, propertyPath); + const isPassed = + isObject(targetValue) && isObject(referenceValue) + ? isMatch(targetValue, referenceValue) + : isEqual(targetValue, referenceValue); + + if (!isPassed) { + failure = { + message: `\`${propertyPath}\` is ${inspect( + targetValue, + )}, when it should be ${inspect(referenceValue)}.`, + }; + } + } else { + failure = { + message: `\`${entryPath}\` should list \`'${propertyPath}': ${inspect( + referenceValue, + )}\`, but does not.`, + }; + } + + return failure ? fail([failure]) : pass(); +} diff --git a/src/rules/index.ts b/src/rules/index.ts index c31fb49..e78ce75 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1,16 +1,27 @@ 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 packageExportsFieldConforms from './package-exports-field-conforms'; +import packageFilesFieldConforms from './package-files-field-conforms'; import packageJestDependenciesConform from './package-jest-dependencies-conform'; +import packageLavamoatTsupConforms from './package-lavamoat-tsup-conforms'; import packageLintDependenciesConform from './package-lint-dependencies-conform'; +import packageMainFieldConforms from './package-main-field-conforms'; +import packageModuleFieldConforms from './package-module-field-conforms'; import packagePackageManagerFieldConforms from './package-package-manager-field-conforms'; import packageTestScriptsConform from './package-test-scripts-conform'; +import packageTypesFieldConforms from './package-types-field-conforms'; +import packageTypescriptDevDependenciesConform from './package-typescript-dev-dependencies-conform'; +import packageTypescriptScriptsConform from './package-typescript-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'; +import requireTsconfig from './require-tsconfig'; +import requireTsconfigBuild from './require-tsconfig-build'; +import requireTsupConfig from './require-tsup-config'; import requireValidPackageManifest from './require-valid-package-manifest'; export const rules = [ @@ -28,4 +39,15 @@ export const rules = [ packageJestDependenciesConform, requireJestConfig, packageTestScriptsConform, + requireTsconfig, + requireTsconfigBuild, + requireTsupConfig, + packageTypescriptDevDependenciesConform, + packageTypescriptScriptsConform, + packageExportsFieldConforms, + packageMainFieldConforms, + packageModuleFieldConforms, + packageTypesFieldConforms, + packageFilesFieldConforms, + packageLavamoatTsupConforms, ] 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 03f7daa..fe9e2f8 100644 --- a/src/rules/package-engines-node-field-conforms.test.ts +++ b/src/rules/package-engines-node-field-conforms.test.ts @@ -2,7 +2,11 @@ import { writeFile } from '@metamask/utils/node'; import path from 'path'; import packageEnginesNodeFieldConforms from './package-engines-node-field-conforms'; -import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; import { fail, pass } from '../rule-helpers'; describe('Rule: package-engines-node-field-conforms', () => { @@ -14,12 +18,7 @@ describe('Rule: package-engines-node-field-conforms', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: 'jest && jest-it-up' }, - }), + buildPackageManifestMock({ engines: { node: '1.0.0' } }), ); const project = buildMetaMaskRepository({ shortname: 'project', @@ -27,12 +26,7 @@ describe('Rule: package-engines-node-field-conforms', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: 'jest && jest-it-up' }, - }), + buildPackageManifestMock({ engines: { node: '1.0.0' } }), ); const result = await packageEnginesNodeFieldConforms.execute({ @@ -56,12 +50,7 @@ describe('Rule: package-engines-node-field-conforms', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test1' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: 'jest && jest-it-up' }, - }), + buildPackageManifestMock({ engines: { node: '1.0.0' } }), ); const project = buildMetaMaskRepository({ shortname: 'project', @@ -69,12 +58,7 @@ describe('Rule: package-engines-node-field-conforms', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test2' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: 'jest && jest-it-up' }, - }), + buildPackageManifestMock({ engines: { node: 'wrong version' } }), ); const result = await packageEnginesNodeFieldConforms.execute({ @@ -88,7 +72,8 @@ describe('Rule: package-engines-node-field-conforms', () => { passed: false, failures: [ { - message: '`engines.node` is "test2", when it should be "test1".', + message: + '`engines.node` is "wrong version", when it should be "1.0.0".', }, ], }); diff --git a/src/rules/package-exports-field-conforms.test.ts b/src/rules/package-exports-field-conforms.test.ts new file mode 100644 index 0000000..b8a6b7b --- /dev/null +++ b/src/rules/package-exports-field-conforms.test.ts @@ -0,0 +1,111 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageExportsFieldConforms from './package-exports-field-conforms'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-exports-field-conforms', () => { + it('passes if the "exports" field in the project\'s package.json matches the 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'), + buildPackageManifestMock({ + exports: { + '.': { + test: 'test-pack', + }, + './package.json': 'test', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + exports: { + '.': { + test: 'test-pack', + }, + './package.json': 'test', + extra: 'export', + }, + }), + ); + + const result = await packageExportsFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it('fails if the "exports" field in the project\'s package.json does not match the 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'), + buildPackageManifestMock({ + exports: { + '.': { + test: 'test-pack', + }, + './package.json': 'test', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + exports: { + '.': { + test: 'test', + }, + './package.json': 'test', + }, + }), + ); + + const result = await packageExportsFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`exports` is { '.': { test: 'test' }, './package.json': 'test' }, when it should be { '.': { test: 'test-pack' }, './package.json': 'test' }.", + }, + ], + }); + }); + }); +}); diff --git a/src/rules/package-exports-field-conforms.ts b/src/rules/package-exports-field-conforms.ts new file mode 100644 index 0000000..4a19862 --- /dev/null +++ b/src/rules/package-exports-field-conforms.ts @@ -0,0 +1,15 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { packageManifestPropertiesConform } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.PackageExportsFieldConforms, + description: 'Does the `exports` field in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async (ruleExecutionArguments) => { + return packageManifestPropertiesConform( + ['exports'], + ruleExecutionArguments, + ); + }, +}); diff --git a/src/rules/package-files-field-conforms.test.ts b/src/rules/package-files-field-conforms.test.ts new file mode 100644 index 0000000..8259bcb --- /dev/null +++ b/src/rules/package-files-field-conforms.test.ts @@ -0,0 +1,82 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageFilesFieldConforms from './package-files-field-conforms'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-files-field-conforms', () => { + it('passes if the "files" field in the project\'s package.json matches the 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'), + buildPackageManifestMock({ files: ['test-files'] }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ files: ['test-files'] }), + ); + + const result = await packageFilesFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it('fails if the "files" field in the project\'s package.json does not match the 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'), + buildPackageManifestMock({ files: ['test-files'] }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ files: ['test'] }), + ); + + const result = await packageFilesFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`files` is [ 'test' ], when it should be [ 'test-files' ].", + }, + ], + }); + }); + }); +}); diff --git a/src/rules/package-files-field-conforms.ts b/src/rules/package-files-field-conforms.ts new file mode 100644 index 0000000..02b58cd --- /dev/null +++ b/src/rules/package-files-field-conforms.ts @@ -0,0 +1,12 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { packageManifestPropertiesConform } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.PackageFilesFieldConforms, + description: 'Does the `files` field in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async (ruleExecutionArguments) => { + return packageManifestPropertiesConform(['files'], ruleExecutionArguments); + }, +}); diff --git a/src/rules/package-jest-dependencies-conform.test.ts b/src/rules/package-jest-dependencies-conform.test.ts index 128bae3..cc03f82 100644 --- a/src/rules/package-jest-dependencies-conform.test.ts +++ b/src/rules/package-jest-dependencies-conform.test.ts @@ -2,7 +2,11 @@ import { writeFile } from '@metamask/utils/node'; import path from 'path'; import packageJestDependenciesConform from './package-jest-dependencies-conform'; -import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; import { fail, pass } from '../rule-helpers'; describe('Rule: package-jest-dependencies-conform', () => { @@ -14,16 +18,8 @@ describe('Rule: package-jest-dependencies-conform', () => { }); 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: '', - }, + buildPackageManifestMock({ + devDependencies: { jest: '1.0.0', 'jest-it-up': '1.0.0' }, }), ); const project = buildMetaMaskRepository({ @@ -32,16 +28,8 @@ describe('Rule: package-jest-dependencies-conform', () => { }); 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: '', - }, + buildPackageManifestMock({ + devDependencies: { jest: '1.0.0', 'jest-it-up': '1.0.0' }, }), ); @@ -66,16 +54,8 @@ describe('Rule: package-jest-dependencies-conform', () => { }); 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: '', - }, + buildPackageManifestMock({ + devDependencies: { jest: '1.0.0', 'jest-it-up': '1.0.0' }, }), ); const project = buildMetaMaskRepository({ @@ -84,16 +64,8 @@ describe('Rule: package-jest-dependencies-conform', () => { }); 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: '', - }, + buildPackageManifestMock({ + devDependencies: { jest: '0.0.1', 'jest-it-up': '1.0.0' }, }), ); @@ -107,7 +79,7 @@ describe('Rule: package-jest-dependencies-conform', () => { expect(result).toStrictEqual({ passed: false, failures: [ - { message: '`jest` is "1.0.0", when it should be "1.1.0".' }, + { message: '`jest` is "0.0.1", when it should be "1.0.0".' }, ], }); }); @@ -121,16 +93,8 @@ describe('Rule: package-jest-dependencies-conform', () => { }); 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: '', - }, + buildPackageManifestMock({ + devDependencies: { jest: '1.0.0', 'jest-it-up': '1.0.0' }, }), ); const project = buildMetaMaskRepository({ @@ -139,15 +103,8 @@ describe('Rule: package-jest-dependencies-conform', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { - 'jest-it-up': '1.0.0', - }, - scripts: { - test: '', - }, + buildPackageManifestMock({ + devDependencies: { 'jest-it-up': '1.0.0' }, }), ); @@ -163,7 +120,7 @@ describe('Rule: package-jest-dependencies-conform', () => { failures: [ { message: - '`package.json` should list `"jest": "1.1.0"` in `devDependencies`, but does not.', + '`package.json` should list `"jest": "1.0.0"` in `devDependencies`, but does not.', }, ], }); @@ -178,15 +135,8 @@ describe('Rule: package-jest-dependencies-conform', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { - 'jest-it-up': '1.0.0', - }, - scripts: { - test: '', - }, + buildPackageManifestMock({ + devDependencies: { 'jest-it-up': '1.0.0' }, }), ); const project = buildMetaMaskRepository({ @@ -195,16 +145,8 @@ describe('Rule: package-jest-dependencies-conform', () => { }); 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: '', - }, + buildPackageManifestMock({ + devDependencies: { jest: '1.0.0', 'jest-it-up': '1.0.0' }, }), ); diff --git a/src/rules/package-lavamoat-tsup-conforms.test.ts b/src/rules/package-lavamoat-tsup-conforms.test.ts new file mode 100644 index 0000000..13017c3 --- /dev/null +++ b/src/rules/package-lavamoat-tsup-conforms.test.ts @@ -0,0 +1,186 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageLavamoatTsupConforms from './package-lavamoat-tsup-conforms'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-lavamoat-tsup-conforms', () => { + it('passes if the project\'s and template\'s package manifests list "tsup>esbuild" in lavamoat.allowScripts and the values match', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + lavamoat: { + allowScripts: { + 'tsup>esbuild': true, + }, + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + lavamoat: { + allowScripts: { + 'tsup>esbuild': true, + 'another-package': false, + }, + }, + }), + ); + const result = await packageLavamoatTsupConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ passed: true }); + }); + }); + + it('fails if the project\'s and template\'s package manifests list "tsup>esbuild" in lavamoat.allowScripts, but the values do not match', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + lavamoat: { + allowScripts: { + 'tsup>esbuild': true, + }, + }, + }), + ); + + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + lavamoat: { + allowScripts: { + 'tsup>esbuild': false, + }, + }, + }), + ); + const result = await packageLavamoatTsupConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: '`tsup>esbuild` is false, when it should be true.', + }, + ], + }); + }); + }); + + it("fails if the project's package manifest has lavamoat and allowScripts, but does not contain tsup>ebuild", async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + lavamoat: { + allowScripts: { + 'tsup>esbuild': true, + }, + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + lavamoat: { + allowScripts: { + test: true, + }, + }, + }), + ); + const result = await packageLavamoatTsupConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`package.json` should list `'tsup>esbuild': true`, but does not.", + }, + ], + }); + }); + }); + + it('passes if the project does not contain lavamoat and allowScripts', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + lavamoat: { + allowScripts: { + 'tsup>esbuild': true, + }, + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock(), + ); + const result = await packageLavamoatTsupConforms.execute({ + template, + project, + pass, + fail, + }); + expect(result).toStrictEqual({ passed: true }); + }); + }); +}); diff --git a/src/rules/package-lavamoat-tsup-conforms.ts b/src/rules/package-lavamoat-tsup-conforms.ts new file mode 100644 index 0000000..08b59ee --- /dev/null +++ b/src/rules/package-lavamoat-tsup-conforms.ts @@ -0,0 +1,33 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { dataConform } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.PackageLavamoatTsupConforms, + description: 'Does LavaMoat allow scripts for `tsup>esbuild`?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async ({ project, template, pass }) => { + const entryPath = 'package.json'; + const templateManifest = await template.fs.readJsonFile(entryPath); + const projectManifest = await project.fs.readJsonFile(entryPath); + + type Key = keyof typeof templateManifest; + const templateLavamoat = templateManifest?.['lavamoat' as Key]; + const projectLavamoat = projectManifest?.['lavamoat' as Key]; + if (templateLavamoat && projectLavamoat) { + type ChildKey = keyof typeof templateLavamoat; + const templateAllowScripts = templateLavamoat['allowScripts' as ChildKey]; + const projectAllowScripts = projectLavamoat['allowScripts' as ChildKey]; + if (projectAllowScripts) { + return dataConform( + templateAllowScripts, + projectAllowScripts, + 'tsup>esbuild', + entryPath, + ); + } + } + + return pass(); + }, +}); diff --git a/src/rules/package-lint-dependencies-conform.test.ts b/src/rules/package-lint-dependencies-conform.test.ts index ee800bf..bcf7f48 100644 --- a/src/rules/package-lint-dependencies-conform.test.ts +++ b/src/rules/package-lint-dependencies-conform.test.ts @@ -2,7 +2,11 @@ import { writeFile } from '@metamask/utils/node'; import path from 'path'; import packageLintDependenciesConform from './package-lint-dependencies-conform'; -import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; import { fail, pass } from '../rule-helpers'; describe('Rule: package-lint-dependencies-conform', () => { @@ -14,20 +18,17 @@ describe('Rule: package-lint-dependencies-conform', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, + buildPackageManifestMock({ devDependencies: { + eslint: '1.0.0', '@metamask/eslint-config-foo': '1.0.0', '@typescript-eslint/foo': '1.0.0', - eslint: '1.0.0', 'eslint-plugin-foo': '1.0.0', 'eslint-config-foo': '1.0.0', prettier: '1.0.0', 'prettier-plugin-foo': '1.0.0', 'prettier-config-foo': '1.0.0', }, - scripts: { test: '' }, }), ); const project = buildMetaMaskRepository({ @@ -36,20 +37,17 @@ describe('Rule: package-lint-dependencies-conform', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, + buildPackageManifestMock({ devDependencies: { + eslint: '1.0.0', '@metamask/eslint-config-foo': '1.0.0', '@typescript-eslint/foo': '1.0.0', - eslint: '1.0.0', 'eslint-plugin-foo': '1.0.0', 'eslint-config-foo': '1.0.0', prettier: '1.0.0', 'prettier-plugin-foo': '1.0.0', 'prettier-config-foo': '1.0.0', }, - scripts: { test: '' }, }), ); @@ -74,11 +72,17 @@ describe('Rule: package-lint-dependencies-conform', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { eslint: '1.1.0' }, - scripts: { test: '' }, + buildPackageManifestMock({ + devDependencies: { + eslint: '1.0.0', + '@metamask/eslint-config-foo': '1.0.0', + '@typescript-eslint/foo': '1.0.0', + 'eslint-plugin-foo': '1.0.0', + 'eslint-config-foo': '1.0.0', + prettier: '1.0.0', + 'prettier-plugin-foo': '1.0.0', + 'prettier-config-foo': '1.0.0', + }, }), ); const project = buildMetaMaskRepository({ @@ -87,11 +91,17 @@ describe('Rule: package-lint-dependencies-conform', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: '' }, + buildPackageManifestMock({ + devDependencies: { + eslint: '0.0.1', + '@metamask/eslint-config-foo': '1.0.0', + '@typescript-eslint/foo': '1.0.0', + 'eslint-plugin-foo': '1.0.0', + 'eslint-config-foo': '1.0.0', + prettier: '1.0.0', + 'prettier-plugin-foo': '1.0.0', + 'prettier-config-foo': '1.0.0', + }, }), ); @@ -105,7 +115,7 @@ describe('Rule: package-lint-dependencies-conform', () => { expect(result).toStrictEqual({ passed: false, failures: [ - { message: '`eslint` is "1.0.0", when it should be "1.1.0".' }, + { message: '`eslint` is "0.0.1", when it should be "1.0.0".' }, ], }); }); @@ -119,11 +129,17 @@ describe('Rule: package-lint-dependencies-conform', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { eslint: '1.1.0' }, - scripts: { test: '' }, + buildPackageManifestMock({ + devDependencies: { + eslint: '1.0.0', + '@metamask/eslint-config-foo': '1.0.0', + '@typescript-eslint/foo': '1.0.0', + 'eslint-plugin-foo': '1.0.0', + 'eslint-config-foo': '1.0.0', + prettier: '1.0.0', + 'prettier-plugin-foo': '1.0.0', + 'prettier-config-foo': '1.0.0', + }, }), ); const project = buildMetaMaskRepository({ @@ -132,11 +148,16 @@ describe('Rule: package-lint-dependencies-conform', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { testlint: '1.0.0' }, - scripts: { test: '' }, + buildPackageManifestMock({ + devDependencies: { + '@metamask/eslint-config-foo': '1.0.0', + '@typescript-eslint/foo': '1.0.0', + 'eslint-plugin-foo': '1.0.0', + 'eslint-config-foo': '1.0.0', + prettier: '1.0.0', + 'prettier-plugin-foo': '1.0.0', + 'prettier-config-foo': '1.0.0', + }, }), ); @@ -152,7 +173,7 @@ describe('Rule: package-lint-dependencies-conform', () => { failures: [ { message: - '`package.json` should list `"eslint": "1.1.0"` in `devDependencies`, but does not.', + '`package.json` should list `"eslint": "1.0.0"` in `devDependencies`, but does not.', }, ], }); @@ -167,14 +188,7 @@ describe('Rule: package-lint-dependencies-conform', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { - '@metamask/test-config-foo': '1.0.0', - }, - scripts: { test: '' }, - }), + buildPackageManifestMock(), ); const project = buildMetaMaskRepository({ shortname: 'project', @@ -182,20 +196,17 @@ describe('Rule: package-lint-dependencies-conform', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, + buildPackageManifestMock({ devDependencies: { + eslint: '1.0.0', '@metamask/eslint-config-foo': '1.0.0', '@typescript-eslint/foo': '1.0.0', - eslint: '1.0.0', 'eslint-plugin-foo': '1.0.0', 'eslint-config-foo': '1.0.0', prettier: '1.0.0', 'prettier-plugin-foo': '1.0.0', 'prettier-config-foo': '1.0.0', }, - scripts: { test: '' }, }), ); diff --git a/src/rules/package-main-field-conforms.test.ts b/src/rules/package-main-field-conforms.test.ts new file mode 100644 index 0000000..822d929 --- /dev/null +++ b/src/rules/package-main-field-conforms.test.ts @@ -0,0 +1,89 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageMainFieldConforms from './package-main-field-conforms'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-main-field-conforms', () => { + it('passes if the "main" field in the project\'s package.json matches the 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'), + buildPackageManifestMock({ + main: 'test-main', + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + main: 'test-main', + }), + ); + + const result = await packageMainFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it('fails if the "main" field in the project\'s package.json does not match the 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'), + buildPackageManifestMock({ + main: 'test-main', + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + main: 'test', + }), + ); + + const result = await packageMainFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: "`main` is 'test', when it should be 'test-main'.", + }, + ], + }); + }); + }); +}); diff --git a/src/rules/package-main-field-conforms.ts b/src/rules/package-main-field-conforms.ts new file mode 100644 index 0000000..91a8c85 --- /dev/null +++ b/src/rules/package-main-field-conforms.ts @@ -0,0 +1,12 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { packageManifestPropertiesConform } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.PackageMainFieldConforms, + description: 'Does the `main` field in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async (ruleExecutionArguments) => { + return packageManifestPropertiesConform(['main'], ruleExecutionArguments); + }, +}); diff --git a/src/rules/package-module-field-conforms.test.ts b/src/rules/package-module-field-conforms.test.ts new file mode 100644 index 0000000..11f6e4f --- /dev/null +++ b/src/rules/package-module-field-conforms.test.ts @@ -0,0 +1,89 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageModuleFieldConforms from './package-module-field-conforms'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-module-field-conforms', () => { + it('passes if the "module" field in the project\'s package.json matches the 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'), + buildPackageManifestMock({ + module: 'test-module', + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + module: 'test-module', + }), + ); + + const result = await packageModuleFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it('fails if the "module" field in the project\'s package.json does not match the 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'), + buildPackageManifestMock({ + module: 'test-module', + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + module: 'test', + }), + ); + + const result = await packageModuleFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: "`module` is 'test', when it should be 'test-module'.", + }, + ], + }); + }); + }); +}); diff --git a/src/rules/package-module-field-conforms.ts b/src/rules/package-module-field-conforms.ts new file mode 100644 index 0000000..822b43b --- /dev/null +++ b/src/rules/package-module-field-conforms.ts @@ -0,0 +1,12 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { packageManifestPropertiesConform } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.PackageModuleFieldConforms, + description: 'Does the `module` field in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async (ruleExecutionArguments) => { + return packageManifestPropertiesConform(['module'], ruleExecutionArguments); + }, +}); diff --git a/src/rules/package-package-manager-field-conforms.test.ts b/src/rules/package-package-manager-field-conforms.test.ts index fce2622..6eef8c6 100644 --- a/src/rules/package-package-manager-field-conforms.test.ts +++ b/src/rules/package-package-manager-field-conforms.test.ts @@ -2,7 +2,11 @@ import { writeFile } from '@metamask/utils/node'; import path from 'path'; import packageManagerFieldConforms from './package-package-manager-field-conforms'; -import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; import { fail, pass } from '../rule-helpers'; describe('Rule: package-manager-field-conforms', () => { @@ -14,11 +18,8 @@ describe('Rule: package-manager-field-conforms', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: '' }, + buildPackageManifestMock({ + packageManager: 'yarn', }), ); const project = buildMetaMaskRepository({ @@ -27,11 +28,8 @@ describe('Rule: package-manager-field-conforms', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: '' }, + buildPackageManifestMock({ + packageManager: 'yarn', }), ); @@ -56,11 +54,8 @@ describe('Rule: package-manager-field-conforms', () => { }); await writeFile( path.join(template.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'a', - engines: { node: 'test' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: '' }, + buildPackageManifestMock({ + packageManager: 'yarn', }), ); const project = buildMetaMaskRepository({ @@ -69,11 +64,8 @@ describe('Rule: package-manager-field-conforms', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'b', - engines: { node: 'test' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: '' }, + buildPackageManifestMock({ + packageManager: 'test', }), ); @@ -87,7 +79,7 @@ describe('Rule: package-manager-field-conforms', () => { expect(result).toStrictEqual({ passed: false, failures: [ - { message: '`packageManager` is "b", when it should be "a".' }, + { message: '`packageManager` is "test", when it should be "yarn".' }, ], }); }); diff --git a/src/rules/package-test-scripts-conform.test.ts b/src/rules/package-test-scripts-conform.test.ts index 6197b0c..a0dd33a 100644 --- a/src/rules/package-test-scripts-conform.test.ts +++ b/src/rules/package-test-scripts-conform.test.ts @@ -2,7 +2,11 @@ import { writeFile } from '@metamask/utils/node'; import path from 'path'; import packageTestScriptsConform from './package-test-scripts-conform'; -import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; import { fail, pass } from '../rule-helpers'; describe('Rule: package-test-scripts-conform', () => { @@ -14,17 +18,8 @@ describe('Rule: package-test-scripts-conform', () => { }); 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', - }, + buildPackageManifestMock({ + scripts: { test: 'test script', 'test:watch': 'test watch script' }, }), ); const project = buildMetaMaskRepository({ @@ -33,17 +28,8 @@ describe('Rule: package-test-scripts-conform', () => { }); 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', - }, + buildPackageManifestMock({ + scripts: { test: 'test script', 'test:watch': 'test watch script' }, }), ); @@ -68,14 +54,8 @@ describe('Rule: package-test-scripts-conform', () => { }); 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', - }, + buildPackageManifestMock({ + scripts: { test: 'test script', 'test:watch': 'test watch script' }, }), ); const project = buildMetaMaskRepository({ @@ -84,14 +64,8 @@ describe('Rule: package-test-scripts-conform', () => { }); 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', - }, + buildPackageManifestMock({ + scripts: { test: 'test', 'test:watch': 'test watch script' }, }), ); @@ -105,7 +79,10 @@ describe('Rule: package-test-scripts-conform', () => { expect(result).toStrictEqual({ passed: false, failures: [ - { message: '`test` is "test", when it should be "test script".' }, + { + message: + "`scripts.[test]` is 'test', when it should be 'test script'.", + }, ], }); }); @@ -119,14 +96,8 @@ describe('Rule: package-test-scripts-conform', () => { }); 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', - }, + buildPackageManifestMock({ + scripts: { test: 'test script', 'test:watch': 'test watch script' }, }), ); const project = buildMetaMaskRepository({ @@ -135,13 +106,8 @@ describe('Rule: package-test-scripts-conform', () => { }); 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', - }, + buildPackageManifestMock({ + scripts: { 'test:watch': 'test watch script' }, }), ); @@ -157,7 +123,7 @@ describe('Rule: package-test-scripts-conform', () => { failures: [ { message: - '`package.json` should list `"test": "test script"` in `scripts`, but does not.', + "`package.json` should list `'scripts.[test]': 'test script'`, but does not.", }, ], }); @@ -172,15 +138,8 @@ describe('Rule: package-test-scripts-conform', () => { }); 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', - }, + buildPackageManifestMock({ + scripts: { 'test:watch': 'test watch script' }, }), ); const project = buildMetaMaskRepository({ @@ -189,17 +148,8 @@ describe('Rule: package-test-scripts-conform', () => { }); 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', - }, + buildPackageManifestMock({ + scripts: { test: 'test script', 'test:watch': 'test watch script' }, }), ); @@ -211,7 +161,7 @@ describe('Rule: package-test-scripts-conform', () => { 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.', + 'Could not find `scripts.[test]` in reference `package.json`. This is not the fault of the target `package.json`, 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 index 9d154f7..f5eeee1 100644 --- a/src/rules/package-test-scripts-conform.ts +++ b/src/rules/package-test-scripts-conform.ts @@ -1,68 +1,15 @@ import { buildRule } from './build-rule'; -import { PackageManifestSchema, RuleName } from './types'; -import type { RuleExecutionFailure } from '../execute-rules'; +import { RuleName } from './types'; +import { packageManifestPropertiesConform } from '../rule-helpers'; 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, + execute: async (ruleExecutionArguments) => { + return packageManifestPropertiesConform( + ['scripts.[test]', 'scripts.[test:watch]'], + ruleExecutionArguments, ); - - 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/package-types-field-conforms.test.ts b/src/rules/package-types-field-conforms.test.ts new file mode 100644 index 0000000..d4345f9 --- /dev/null +++ b/src/rules/package-types-field-conforms.test.ts @@ -0,0 +1,81 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageTypesFieldConforms from './package-types-field-conforms'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-types-field-conforms', () => { + it('passes if the "types" field in the project\'s package.json matches the 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'), + buildPackageManifestMock({ types: 'test-types' }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ types: 'test-types' }), + ); + + const result = await packageTypesFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it('fails if the "types" field in the project\'s package.json does not match the 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'), + buildPackageManifestMock({ types: 'test-types' }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ types: 'test' }), + ); + + const result = await packageTypesFieldConforms.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: "`types` is 'test', when it should be 'test-types'.", + }, + ], + }); + }); + }); +}); diff --git a/src/rules/package-types-field-conforms.ts b/src/rules/package-types-field-conforms.ts new file mode 100644 index 0000000..5c2c4b3 --- /dev/null +++ b/src/rules/package-types-field-conforms.ts @@ -0,0 +1,12 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { packageManifestPropertiesConform } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.PackageTypesFieldConforms, + description: 'Does the `types` field in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async (ruleExecutionArguments) => { + return packageManifestPropertiesConform(['types'], ruleExecutionArguments); + }, +}); diff --git a/src/rules/package-typescript-dev-dependencies-conform.test.ts b/src/rules/package-typescript-dev-dependencies-conform.test.ts new file mode 100644 index 0000000..675c0d0 --- /dev/null +++ b/src/rules/package-typescript-dev-dependencies-conform.test.ts @@ -0,0 +1,200 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageTypescriptDevDependenciesConform from './package-typescript-dev-dependencies-conform'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-typescript-dev-dependencies-conform', () => { + it('passes if the typescript related devDependencies of template exist in project with the version matching', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + '@types/node': '1.0.0', + 'ts-node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + '@types/node': '1.0.0', + 'ts-node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', + }, + }), + ); + const result = await packageTypescriptDevDependenciesConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ passed: true }); + }); + }); + + it('fails if the project has the same referenced packages as the template, but its version does not match', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + '@types/node': '1.0.0', + 'ts-node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + '@types/node': '1.0.0', + 'ts-node': '0.0.1', + tsup: '1.0.0', + typescript: '1.0.0', + }, + }), + ); + const result = await packageTypescriptDevDependenciesConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`devDependencies.[ts-node]` is '0.0.1', when it should be '1.0.0'.", + }, + ], + }); + }); + }); + + it('fails if the project does not have the same referenced package as the template', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + '@types/node': '1.0.0', + 'ts-node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + '@types/node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', + }, + }), + ); + const result = await packageTypescriptDevDependenciesConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`package.json` should list `'devDependencies.[ts-node]': '1.0.0'`, but does not.", + }, + ], + }); + }); + }); + + it('throws error if the package does not exist in the template devDependencies', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + '@types/node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + devDependencies: { + '@types/node': '1.0.0', + 'ts-node': '1.0.0', + tsup: '1.0.0', + typescript: '1.0.0', + }, + }), + ); + await expect( + packageTypescriptDevDependenciesConform.execute({ + template, + project, + pass, + fail, + }), + ).rejects.toThrow( + 'Could not find `devDependencies.[ts-node]` in reference `package.json`. This is not the fault of the target `package.json`, but is rather a bug in a rule.', + ); + }); + }); +}); diff --git a/src/rules/package-typescript-dev-dependencies-conform.ts b/src/rules/package-typescript-dev-dependencies-conform.ts new file mode 100644 index 0000000..3414a9f --- /dev/null +++ b/src/rules/package-typescript-dev-dependencies-conform.ts @@ -0,0 +1,22 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { packageManifestPropertiesConform } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.PackageTypescriptDependenciesConform, + description: + 'Do the typescript-related `devDependencies` in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async (ruleExecutionArguments) => { + const requiredPackages = [ + 'devDependencies.[@types/node]', + 'devDependencies.[ts-node]', + 'devDependencies.[tsup]', + 'devDependencies.[typescript]', + ]; + return packageManifestPropertiesConform( + requiredPackages, + ruleExecutionArguments, + ); + }, +}); diff --git a/src/rules/package-typescript-scripts-conform.test.ts b/src/rules/package-typescript-scripts-conform.test.ts new file mode 100644 index 0000000..6ac5896 --- /dev/null +++ b/src/rules/package-typescript-scripts-conform.test.ts @@ -0,0 +1,184 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import packageTypescriptScriptsConform from './package-typescript-scripts-conform'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: package-typescript-scripts-conform', () => { + it('passes if the typescript related scripts in template exist in project and its value matches', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + scripts: { + build: 'test build', + 'build:types': 'test build types', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + scripts: { + build: 'test build', + 'build:types': 'test build types', + }, + }), + ); + const result = await packageTypescriptScriptsConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ passed: true }); + }); + }); + + it('fails if the project has the same referenced scripts as the template, but its value does not match', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + scripts: { + build: 'test build', + 'build:types': 'test build types', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + scripts: { + build: 'test', + 'build:types': 'test build types', + }, + }), + ); + const result = await packageTypescriptScriptsConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`scripts.[build]` is 'test', when it should be 'test build'.", + }, + ], + }); + }); + }); + + it('fails if the project does not have the same referenced script as the template', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + scripts: { + build: 'test build', + 'build:types': 'test build types', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + scripts: { + 'build:types': 'test build types', + }, + }), + ); + const result = await packageTypescriptScriptsConform.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: + "`package.json` should list `'scripts.[build]': 'test build'`, but does not.", + }, + ], + }); + }); + }); + + it('throws error if the script does not exist in the template scripts', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'package.json'), + buildPackageManifestMock({ + scripts: { + 'build:types': 'test build types', + }, + }), + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'package.json'), + buildPackageManifestMock({ + scripts: { + build: 'test build', + 'build:types': 'test build types', + }, + }), + ); + await expect( + packageTypescriptScriptsConform.execute({ + template, + project, + pass, + fail, + }), + ).rejects.toThrow( + 'Could not find `scripts.[build]` in reference `package.json`. This is not the fault of the target `package.json`, but is rather a bug in a rule.', + ); + }); + }); +}); diff --git a/src/rules/package-typescript-scripts-conform.ts b/src/rules/package-typescript-scripts-conform.ts new file mode 100644 index 0000000..1023242 --- /dev/null +++ b/src/rules/package-typescript-scripts-conform.ts @@ -0,0 +1,15 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { packageManifestPropertiesConform } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.PackageTypescriptScriptsConform, + description: 'Do the typescript-related `scripts` in `package.json` conform?', + dependencies: [RuleName.RequireValidPackageManifest], + execute: async (ruleExecutionArguments) => { + return await packageManifestPropertiesConform( + ['scripts.[build]', 'scripts.[build:types]'], + ruleExecutionArguments, + ); + }, +}); diff --git a/src/rules/require-tsconfig-build.test.ts b/src/rules/require-tsconfig-build.test.ts new file mode 100644 index 0000000..cb527e7 --- /dev/null +++ b/src/rules/require-tsconfig-build.test.ts @@ -0,0 +1,73 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import requireTsconfigBuild from './require-tsconfig-build'; +import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: require-tsconfig-build', () => { + it('passes if the project has a tsconfig.build.json', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'tsconfig.build.json'), + 'contents of tsconfig-build', + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'tsconfig.build.json'), + 'contents of tsconfig-build', + ); + + const result = await requireTsconfigBuild.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it('fails with failure message when tsconfig.build.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, 'tsconfig.build.json'), + 'contents of tsconfig-build', + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + + const result = await requireTsconfigBuild.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: '`tsconfig.build.json` does not exist in this project.', + }, + ], + }); + }); + }); +}); diff --git a/src/rules/require-tsconfig-build.ts b/src/rules/require-tsconfig-build.ts new file mode 100644 index 0000000..48c392a --- /dev/null +++ b/src/rules/require-tsconfig-build.ts @@ -0,0 +1,12 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { fileConforms } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.RequireTsConfigBuild, + description: 'Is `tsconfig.build.json` present, and does it conform?', + dependencies: [], + execute: async (ruleExecutionArguments) => { + return await fileConforms('tsconfig.build.json', ruleExecutionArguments); + }, +}); diff --git a/src/rules/require-tsconfig.test.ts b/src/rules/require-tsconfig.test.ts new file mode 100644 index 0000000..8ab7546 --- /dev/null +++ b/src/rules/require-tsconfig.test.ts @@ -0,0 +1,73 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import requireTsconfig from './require-tsconfig'; +import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: require-tsconfig', () => { + it('passes if the project has a tsconfig.json', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'tsconfig.json'), + 'contents of tsconfig', + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'tsconfig.json'), + 'contents of tsconfig', + ); + + const result = await requireTsconfig.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it('fails with failure message when tsconfig.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, 'tsconfig.json'), + 'contents of tsconfig', + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + + const result = await requireTsconfig.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: '`tsconfig.json` does not exist in this project.', + }, + ], + }); + }); + }); +}); diff --git a/src/rules/require-tsconfig.ts b/src/rules/require-tsconfig.ts new file mode 100644 index 0000000..1e77932 --- /dev/null +++ b/src/rules/require-tsconfig.ts @@ -0,0 +1,12 @@ +import { buildRule } from './build-rule'; +import { RuleName } from './types'; +import { fileConforms } from '../rule-helpers'; + +export default buildRule({ + name: RuleName.RequireTsConfig, + description: 'Is `tsconfig.json` present, and does it conform?', + dependencies: [], + execute: async (ruleExecutionArguments) => { + return await fileConforms('tsconfig.json', ruleExecutionArguments); + }, +}); diff --git a/src/rules/require-tsup-config.test.ts b/src/rules/require-tsup-config.test.ts new file mode 100644 index 0000000..c04d4d8 --- /dev/null +++ b/src/rules/require-tsup-config.test.ts @@ -0,0 +1,73 @@ +import { writeFile } from '@metamask/utils/node'; +import path from 'path'; + +import requireTsupConfig from './require-tsup-config'; +import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { fail, pass } from '../rule-helpers'; + +describe('Rule: require-tsup-config', () => { + it('passes if the project has a tsup.config.ts', async () => { + await withinSandbox(async (sandbox) => { + const template = buildMetaMaskRepository({ + shortname: 'template', + directoryPath: path.join(sandbox.directoryPath, 'template'), + }); + await writeFile( + path.join(template.directoryPath, 'tsup.config.ts'), + 'contents of tsup-config', + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + await writeFile( + path.join(project.directoryPath, 'tsup.config.ts'), + 'contents of tsup-config', + ); + + const result = await requireTsupConfig.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: true, + }); + }); + }); + + it('fails with failure message when tsup.config.ts 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, 'tsup.config.ts'), + 'contents of tsup-config', + ); + const project = buildMetaMaskRepository({ + shortname: 'project', + directoryPath: path.join(sandbox.directoryPath, 'project'), + }); + + const result = await requireTsupConfig.execute({ + template, + project, + pass, + fail, + }); + + expect(result).toStrictEqual({ + passed: false, + failures: [ + { + message: '`tsup.config.ts` does not exist in this project.', + }, + ], + }); + }); + }); +}); diff --git a/src/rules/require-tsup-config.ts b/src/rules/require-tsup-config.ts new file mode 100644 index 0000000..19ea200 --- /dev/null +++ b/src/rules/require-tsup-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.RequireTsupConfig, + description: 'Is `tsup.config.ts` present, and does it conform?', + dependencies: [], + execute: async (ruleExecutionArguments) => { + return await fileConforms('tsup.config.ts', ruleExecutionArguments); + }, +}); diff --git a/src/rules/require-valid-package-manifest.test.ts b/src/rules/require-valid-package-manifest.test.ts index bffa2cf..8fc3433 100644 --- a/src/rules/require-valid-package-manifest.test.ts +++ b/src/rules/require-valid-package-manifest.test.ts @@ -2,7 +2,11 @@ import { writeFile } from '@metamask/utils/node'; import path from 'path'; import requireValidPackageManifest from './require-valid-package-manifest'; -import { buildMetaMaskRepository, withinSandbox } from '../../tests/helpers'; +import { + buildMetaMaskRepository, + buildPackageManifestMock, + withinSandbox, +} from '../../tests/helpers'; import { fail, pass } from '../rule-helpers'; describe('Rule: require-package-manifest', () => { @@ -14,12 +18,7 @@ describe('Rule: require-package-manifest', () => { }); await writeFile( path.join(project.directoryPath, 'package.json'), - JSON.stringify({ - packageManager: 'foo', - engines: { node: 'test' }, - devDependencies: { eslint: '1.0.0' }, - scripts: { test: 'test script' }, - }), + buildPackageManifestMock(), ); const result = await requireValidPackageManifest.execute({ @@ -83,7 +82,7 @@ describe('Rule: require-package-manifest', () => { failures: [ { message: - 'Invalid `package.json`: Missing `packageManager`; Missing `engines`; Missing `scripts`; Missing `devDependencies`.', + 'Invalid `package.json`: Missing `packageManager`; Missing `engines`; Missing `exports`; Missing `main`; Missing `module`; Missing `types`; Missing `files`; Missing `scripts`; Missing `devDependencies`.', }, ], }); diff --git a/src/rules/types.ts b/src/rules/types.ts index f3a8e68..b0cf57b 100644 --- a/src/rules/types.ts +++ b/src/rules/types.ts @@ -1,4 +1,4 @@ -import { type, string, record } from 'superstruct'; +import { type, string, record, array } from 'superstruct'; /** * All of the known rules. @@ -18,6 +18,17 @@ export enum RuleName { PackageJestDependenciesConform = 'package-jest-dependencies-conform', RequireJestConfig = 'require-jest-config', PackageTestScriptsConform = 'package-test-scripts-conform', + RequireTsConfig = 'require-tsconfig', + RequireTsConfigBuild = 'require-tsconfig-build', + RequireTsupConfig = 'require-tsup-config', + PackageTypescriptDependenciesConform = 'package-typescript-dependencies-conform', + PackageTypescriptScriptsConform = 'package-typescript-scripts-conform', + PackageExportsFieldConforms = 'package-exports-field-conforms', + PackageMainFieldConforms = 'package-main-field-conforms', + PackageModuleFieldConforms = 'package-module-field-conforms', + PackageTypesFieldConforms = 'package-types-field-conforms', + PackageFilesFieldConforms = 'package-files-field-conforms', + PackageLavamoatTsupConforms = 'package-lavamoat-tsup-conforms', } export const PackageManifestSchema = type({ @@ -25,6 +36,14 @@ export const PackageManifestSchema = type({ engines: type({ node: string(), }), + exports: type({ + '.': record(string(), string()), + './package.json': string(), + }), + main: string(), + module: string(), + types: string(), + files: array(string()), scripts: record(string(), string()), devDependencies: record(string(), string()), }); diff --git a/tests/helpers.ts b/tests/helpers.ts index 40d46ff..756065a 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -143,3 +143,29 @@ export function buildMetaMaskRepository({ fs, }; } + +/** + * Provides package manifest in string format with all the required properties for testing. + * @param overrides - Properties to override. + * @returns PackageManifestMock. + */ +export function buildPackageManifestMock( + overrides?: Record, +): string { + const validPackageManifestMock = { + packageManager: 'yarn', + engines: { node: '1.0.0' }, + main: 'test-main', + module: 'test-module', + types: 'test-types', + files: ['test-files'], + exports: { + '.': {}, + './package.json': 'test', + }, + devDependencies: {}, + scripts: {}, + }; + + return JSON.stringify({ ...validPackageManifestMock, ...overrides }); +} diff --git a/yarn.lock b/yarn.lock index 3fd32f0..ab1a2ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1000,6 +1000,7 @@ __metadata: "@swc/cli": ^0.1.62 "@swc/core": ^1.3.66 "@types/jest": ^28.1.6 + "@types/lodash": ^4.14.202 "@types/node": ^16 "@typescript-eslint/eslint-plugin": ^5.43.0 "@typescript-eslint/parser": ^5.43.0 @@ -1018,6 +1019,7 @@ __metadata: jest: ^28.1.3 jest-it-up: ^2.0.2 jest-mock-extended: ^3.0.5 + lodash: ^4.17.21 prettier: ^2.7.1 prettier-plugin-packagejson: ^2.3.0 rimraf: ^3.0.2 @@ -1567,6 +1569,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.14.202": + version: 4.14.202 + resolution: "@types/lodash@npm:4.14.202" + checksum: a91acf3564a568c6f199912f3eb2c76c99c5a0d7e219394294213b3f2d54f672619f0fde4da22b29dc5d4c31457cd799acc2e5cb6bd90f9af04a1578483b6ff7 + languageName: node + linkType: hard + "@types/minimatch@npm:*, @types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5"