chore(repo): add conformance rule for our package.json files (#29078)
This commit is contained in:
parent
2eb524307b
commit
dfd5014792
40
nx.json
40
nx.json
@ -246,5 +246,43 @@
|
||||
"nxCloudUrl": "https://staging.nx.app",
|
||||
"parallel": 1,
|
||||
"bust": 1,
|
||||
"defaultBase": "master"
|
||||
"defaultBase": "master",
|
||||
"conformance": {
|
||||
"rules": [
|
||||
{
|
||||
"rule": "@nx/workspace-plugin/conformance-rules/project-package-json",
|
||||
"projects": [
|
||||
"!.",
|
||||
"!create-nx-*",
|
||||
"!cypress",
|
||||
"!detox",
|
||||
"!devkit",
|
||||
"!esbuild",
|
||||
"!eslint-plugin",
|
||||
"!eslint",
|
||||
"!expo",
|
||||
"!express",
|
||||
"!jest",
|
||||
"!js",
|
||||
"!module-federation",
|
||||
"!nest",
|
||||
"!next",
|
||||
"!node",
|
||||
"!nuxt",
|
||||
"!packages/nx/**",
|
||||
"!plugin",
|
||||
"!react-native",
|
||||
"!react",
|
||||
"!rollup",
|
||||
"!rsbuild",
|
||||
"!rspack",
|
||||
"!storybook",
|
||||
"!vue",
|
||||
"!web",
|
||||
"!webpack",
|
||||
"!workspace"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
24
package.json
24
package.json
@ -50,9 +50,9 @@
|
||||
"@eslint/eslintrc": "^2.1.1",
|
||||
"@eslint/js": "^8.48.0",
|
||||
"@floating-ui/react": "0.26.6",
|
||||
"@jest/reporters": "^29.4.1",
|
||||
"@jest/test-result": "^29.4.1",
|
||||
"@jest/types": "^29.4.1",
|
||||
"@jest/reporters": "29.7.0",
|
||||
"@jest/test-result": "29.7.0",
|
||||
"@jest/types": "29.6.3",
|
||||
"@module-federation/enhanced": "0.7.6",
|
||||
"@module-federation/sdk": "0.7.6",
|
||||
"@monodon/rust": "2.1.1",
|
||||
@ -79,9 +79,9 @@
|
||||
"@nx/js": "20.3.0-beta.0",
|
||||
"@nx/next": "20.3.0-beta.0",
|
||||
"@nx/playwright": "20.3.0-beta.0",
|
||||
"@nx/powerpack-conformance": "1.1.0-beta.9",
|
||||
"@nx/powerpack-enterprise-cloud": "1.1.0-beta.9",
|
||||
"@nx/powerpack-license": "1.1.0-beta.9",
|
||||
"@nx/powerpack-conformance": "1.1.1-alpha.1",
|
||||
"@nx/powerpack-enterprise-cloud": "1.1.1-alpha.1",
|
||||
"@nx/powerpack-license": "1.1.1-alpha.1",
|
||||
"@nx/react": "20.3.0-beta.0",
|
||||
"@nx/rsbuild": "20.3.0-beta.0",
|
||||
"@nx/rspack": "20.3.0-beta.0",
|
||||
@ -219,13 +219,13 @@
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-config": "^29.4.1",
|
||||
"jest-diff": "^29.4.1",
|
||||
"jest-config": "29.7.0",
|
||||
"jest-diff": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-environment-node": "^29.4.1",
|
||||
"jest-resolve": "^29.4.1",
|
||||
"jest-runtime": "^29.4.1",
|
||||
"jest-util": "^29.4.1",
|
||||
"jest-environment-node": "29.7.0",
|
||||
"jest-resolve": "29.7.0",
|
||||
"jest-runtime": "29.7.0",
|
||||
"jest-util": "29.7.0",
|
||||
"js-tokens": "^4.0.0",
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"jsonc-parser": "3.2.0",
|
||||
|
||||
@ -60,5 +60,8 @@
|
||||
},
|
||||
"nx-migrations": {
|
||||
"migrations": "./migrations.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
||||
643
pnpm-lock.yaml
generated
643
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -63,7 +63,10 @@ module.exports = function (path, options) {
|
||||
}
|
||||
// Try to use the defaultResolver
|
||||
try {
|
||||
if (path.startsWith('@nx/')) throw new Error('custom resolution');
|
||||
// powerpack packages are installed via npm and resolved like any other packages
|
||||
if (path.startsWith('@nx/') && !path.startsWith('@nx/powerpack-')) {
|
||||
throw new Error('custom resolution');
|
||||
}
|
||||
if (path.startsWith('nx/')) throw new Error('custom resolution');
|
||||
|
||||
if (path.indexOf('@nx/workspace') > -1) {
|
||||
|
||||
@ -0,0 +1,289 @@
|
||||
const mockExistsSync = jest.fn();
|
||||
jest.mock('node:fs', () => {
|
||||
return {
|
||||
...jest.requireActual('node:fs'),
|
||||
existsSync: mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
import { validateProjectPackageJson } from './index';
|
||||
|
||||
const VALID_PACKAGE_JSON_BASE = {
|
||||
name: '@nx/test-project',
|
||||
publishConfig: {
|
||||
access: 'public',
|
||||
},
|
||||
exports: {
|
||||
'./package.json': './package.json',
|
||||
},
|
||||
};
|
||||
|
||||
describe('project-package-json', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
// Unit test the core implementation details of validating the project package.json
|
||||
describe('validateProjectPackageJson()', () => {
|
||||
it('should return no violations for a valid project package.json', () => {
|
||||
const packageJson = {
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
};
|
||||
const sourceProject = 'test-project';
|
||||
const sourceProjectRoot = '/path/to/test-project';
|
||||
const violations = validateProjectPackageJson(
|
||||
packageJson,
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
);
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return a violation if the name is not a string', () => {
|
||||
const packageJson = {
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
};
|
||||
delete packageJson.name;
|
||||
|
||||
const sourceProject = 'test-project';
|
||||
const sourceProjectRoot = '/path/to/test-project';
|
||||
const violations = validateProjectPackageJson(
|
||||
packageJson,
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
);
|
||||
expect(violations).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"file": "/path/to/test-project/package.json",
|
||||
"message": "The project package.json should have a "name" field",
|
||||
"sourceProject": "test-project",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return a violation if the name is not scoped an org that is not @nx', () => {
|
||||
const sourceProject = 'test-project';
|
||||
const sourceProjectRoot = '/path/to/test-project';
|
||||
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
// Should be fine, as not scoped
|
||||
{
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
name: 'test-project',
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toEqual([]);
|
||||
|
||||
// Should return a violation, as scoped to an org that is not @nx
|
||||
const packageJsonWithScope = {
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
name: '@nx-labs/test-project',
|
||||
};
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
packageJsonWithScope,
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"file": "/path/to/test-project/package.json",
|
||||
"message": "The package name should be scoped to the @nx org",
|
||||
"sourceProject": "test-project",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return a violation if a public package does not have publishConfig.access set to public', () => {
|
||||
const sourceProject = 'some-project-name';
|
||||
const sourceProjectRoot = '/path/to/some-project-name';
|
||||
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
// Should be fine, as private
|
||||
{
|
||||
private: true,
|
||||
name: 'test-project',
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toEqual([]);
|
||||
|
||||
// Should return a violation, as not private
|
||||
const packageJsonWithoutPublicAccess = {
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
};
|
||||
delete packageJsonWithoutPublicAccess.publishConfig;
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
packageJsonWithoutPublicAccess,
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"file": "/path/to/some-project-name/package.json",
|
||||
"message": "Public packages should have "publishConfig": { "access": "public" } set in their package.json",
|
||||
"sourceProject": "some-project-name",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return a violation if the project has an executors.json but does not reference it in the package.json', () => {
|
||||
const sourceProject = 'some-project-name';
|
||||
const sourceProjectRoot = '/path/to/some-project-name';
|
||||
|
||||
// The project does not have an executors.json, so no violation
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
{
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toEqual([]);
|
||||
|
||||
// The project has an executors.json
|
||||
mockExistsSync.mockImplementation((path) => {
|
||||
if (path.endsWith('executors.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// The project references the executors.json in the package.json, so no violation
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
{
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
executors: './executors.json',
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toEqual([]);
|
||||
|
||||
// The project does not reference the executors.json in the package.json, so a violation is returned
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
{
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"file": "/path/to/some-project-name/package.json",
|
||||
"message": "The project has an executors.json, but does not reference "./executors.json" in the "executors" field of its package.json",
|
||||
"sourceProject": "some-project-name",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return a violation if the project has an generators.json but does not reference it in the package.json', () => {
|
||||
const sourceProject = 'some-project-name';
|
||||
const sourceProjectRoot = '/path/to/some-project-name';
|
||||
|
||||
// The project does not have an generators.json, so no violation
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
{
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toEqual([]);
|
||||
|
||||
// The project has an generators.json
|
||||
mockExistsSync.mockImplementation((path) => {
|
||||
if (path.endsWith('generators.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// The project references the generators.json in the package.json, so no violation
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
{
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
generators: './generators.json',
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toEqual([]);
|
||||
|
||||
// The project does not reference the generators.json in the package.json, so a violation is returned
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
{
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"file": "/path/to/some-project-name/package.json",
|
||||
"message": "The project has an generators.json, but does not reference "./generators.json" in the "generators" field of its package.json",
|
||||
"sourceProject": "some-project-name",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return a violation if the project does not specify an exports object in the package.json', () => {
|
||||
const sourceProject = 'test-project';
|
||||
const sourceProjectRoot = '/path/to/test-project';
|
||||
|
||||
expect(
|
||||
validateProjectPackageJson(
|
||||
{
|
||||
...VALID_PACKAGE_JSON_BASE,
|
||||
exports: undefined,
|
||||
},
|
||||
sourceProject,
|
||||
sourceProjectRoot,
|
||||
`${sourceProjectRoot}/package.json`
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"file": "/path/to/test-project/package.json",
|
||||
"message": "The project package.json should have an "exports" object specified",
|
||||
"sourceProject": "test-project",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,125 @@
|
||||
import { readJsonFile, workspaceRoot } from '@nx/devkit';
|
||||
import {
|
||||
createConformanceRule,
|
||||
type ProjectFilesViolation,
|
||||
} from '@nx/powerpack-conformance';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export default createConformanceRule<object>({
|
||||
name: 'project-package-json',
|
||||
category: 'consistency',
|
||||
description:
|
||||
'Ensures consistency across our project package.json files within the Nx repo',
|
||||
reporter: 'project-files-reporter',
|
||||
implementation: async ({ projectGraph }) => {
|
||||
const violations: ProjectFilesViolation[] = [];
|
||||
|
||||
for (const project of Object.values(projectGraph.nodes)) {
|
||||
const projectPackageJsonPath = join(
|
||||
workspaceRoot,
|
||||
project.data.root,
|
||||
'package.json'
|
||||
);
|
||||
if (existsSync(projectPackageJsonPath)) {
|
||||
const projectPackageJson = readJsonFile(projectPackageJsonPath);
|
||||
violations.push(
|
||||
...validateProjectPackageJson(
|
||||
projectPackageJson,
|
||||
project.name,
|
||||
project.data.root,
|
||||
projectPackageJsonPath
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
severity: 'medium',
|
||||
details: {
|
||||
violations,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export function validateProjectPackageJson(
|
||||
projectPackageJson: Record<string, unknown>,
|
||||
sourceProject: string,
|
||||
sourceProjectRoot: string,
|
||||
projectPackageJsonPath: string
|
||||
): ProjectFilesViolation[] {
|
||||
const violations: ProjectFilesViolation[] = [];
|
||||
|
||||
// Private packages are exempt from this rule
|
||||
if (projectPackageJson.private === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof projectPackageJson.name !== 'string') {
|
||||
violations.push({
|
||||
message: 'The project package.json should have a "name" field',
|
||||
sourceProject,
|
||||
file: projectPackageJsonPath,
|
||||
});
|
||||
} else {
|
||||
// Ensure that if a scope is used, it is only the @nx scope
|
||||
if (
|
||||
projectPackageJson.name.startsWith('@') &&
|
||||
!projectPackageJson.name.startsWith('@nx/')
|
||||
) {
|
||||
violations.push({
|
||||
message: 'The package name should be scoped to the @nx org',
|
||||
sourceProject,
|
||||
file: projectPackageJsonPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Publish config
|
||||
if ((projectPackageJson.publishConfig as any)?.access !== 'public') {
|
||||
violations.push({
|
||||
message:
|
||||
'Public packages should have "publishConfig": { "access": "public" } set in their package.json',
|
||||
sourceProject,
|
||||
file: projectPackageJsonPath,
|
||||
});
|
||||
}
|
||||
|
||||
// Nx config properties
|
||||
if (existsSync(join(sourceProjectRoot, 'executors.json'))) {
|
||||
if (projectPackageJson.executors !== './executors.json') {
|
||||
violations.push({
|
||||
message:
|
||||
'The project has an executors.json, but does not reference "./executors.json" in the "executors" field of its package.json',
|
||||
sourceProject,
|
||||
file: projectPackageJsonPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (existsSync(join(sourceProjectRoot, 'generators.json'))) {
|
||||
if (projectPackageJson.generators !== './generators.json') {
|
||||
violations.push({
|
||||
message:
|
||||
'The project has an generators.json, but does not reference "./generators.json" in the "generators" field of its package.json',
|
||||
sourceProject,
|
||||
file: projectPackageJsonPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const hasExportsEntries =
|
||||
typeof projectPackageJson.exports === 'object' &&
|
||||
Object.keys(projectPackageJson.exports ?? {}).length > 0;
|
||||
|
||||
if (!hasExportsEntries) {
|
||||
violations.push({
|
||||
message:
|
||||
'The project package.json should have an "exports" object specified',
|
||||
sourceProject,
|
||||
file: projectPackageJsonPath,
|
||||
});
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@ -152,6 +152,9 @@
|
||||
"@nx/webpack/*": ["packages/webpack/*"],
|
||||
"@nx/workspace": ["packages/workspace"],
|
||||
"@nx/workspace-plugin": ["tools/workspace-plugin/src/index.ts"],
|
||||
"@nx/workspace-plugin/conformance-rules/*": [
|
||||
"tools/workspace-plugin/src/conformance-rules/*"
|
||||
],
|
||||
"@nx/workspace/*": ["packages/workspace/*"],
|
||||
"create-nx-workspace": ["packages/create-nx-workspace/index.ts"],
|
||||
"create-nx-workspace/*": ["packages/create-nx-workspace/*"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user