@servicetitan/startup 32.0.1 → 32.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/cli/commands/review/rules/index.d.ts.map +1 -1
  2. package/dist/cli/commands/review/rules/index.js +10 -2
  3. package/dist/cli/commands/review/rules/index.js.map +1 -1
  4. package/dist/cli/commands/review/rules/no-deprecated-content-base.d.ts +13 -0
  5. package/dist/cli/commands/review/rules/no-deprecated-content-base.d.ts.map +1 -0
  6. package/dist/cli/commands/review/rules/no-deprecated-content-base.js +71 -0
  7. package/dist/cli/commands/review/rules/no-deprecated-content-base.js.map +1 -0
  8. package/dist/cli/commands/review/rules/no-direct-peer-dependencies.d.ts +15 -0
  9. package/dist/cli/commands/review/rules/no-direct-peer-dependencies.d.ts.map +1 -0
  10. package/dist/cli/commands/review/rules/no-direct-peer-dependencies.js +75 -0
  11. package/dist/cli/commands/review/rules/no-direct-peer-dependencies.js.map +1 -0
  12. package/dist/cli/commands/review/rules/no-typescript-entry-point.d.ts +2 -2
  13. package/dist/cli/commands/review/rules/no-typescript-entry-point.d.ts.map +1 -1
  14. package/dist/cli/commands/review/rules/no-typescript-entry-point.js +58 -45
  15. package/dist/cli/commands/review/rules/no-typescript-entry-point.js.map +1 -1
  16. package/dist/cli/commands/review/rules/prefer-open-ended-peer-dependencies.d.ts +20 -0
  17. package/dist/cli/commands/review/rules/prefer-open-ended-peer-dependencies.d.ts.map +1 -0
  18. package/dist/cli/commands/review/rules/prefer-open-ended-peer-dependencies.js +145 -0
  19. package/dist/cli/commands/review/rules/prefer-open-ended-peer-dependencies.js.map +1 -0
  20. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.d.ts +22 -0
  21. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.d.ts.map +1 -0
  22. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.js +189 -0
  23. package/dist/cli/commands/review/rules/require-compatible-launch-darkly-sdk.js.map +1 -0
  24. package/dist/cli/commands/review/rules/require-explicit-side-effects.d.ts +2 -1
  25. package/dist/cli/commands/review/rules/require-explicit-side-effects.d.ts.map +1 -1
  26. package/dist/cli/commands/review/rules/require-explicit-side-effects.js +24 -11
  27. package/dist/cli/commands/review/rules/require-explicit-side-effects.js.map +1 -1
  28. package/dist/cli/commands/review/rules/require-one-collection-version.d.ts.map +1 -1
  29. package/dist/cli/commands/review/rules/require-one-collection-version.js +1 -1
  30. package/dist/cli/commands/review/rules/require-one-collection-version.js.map +1 -1
  31. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.d.ts.map +1 -1
  32. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.js +5 -7
  33. package/dist/cli/commands/review/rules/require-project-version-in-root-node-modules.js.map +1 -1
  34. package/dist/cli/commands/review/rules/require-servicetitan-scope.d.ts +2 -1
  35. package/dist/cli/commands/review/rules/require-servicetitan-scope.d.ts.map +1 -1
  36. package/dist/cli/commands/review/rules/require-servicetitan-scope.js +24 -11
  37. package/dist/cli/commands/review/rules/require-servicetitan-scope.js.map +1 -1
  38. package/dist/cli/commands/review/types.d.ts +4 -1
  39. package/dist/cli/commands/review/types.d.ts.map +1 -1
  40. package/dist/cli/commands/review/types.js.map +1 -1
  41. package/dist/cli/commands/review/utils/check-packages.d.ts +7 -0
  42. package/dist/cli/commands/review/utils/check-packages.d.ts.map +1 -0
  43. package/dist/cli/commands/review/utils/check-packages.js +30 -0
  44. package/dist/cli/commands/review/utils/check-packages.js.map +1 -0
  45. package/dist/cli/commands/review/utils/index.d.ts +1 -0
  46. package/dist/cli/commands/review/utils/index.d.ts.map +1 -1
  47. package/dist/cli/commands/review/utils/index.js +1 -0
  48. package/dist/cli/commands/review/utils/index.js.map +1 -1
  49. package/dist/cli/commands/test/runners/vitest.js +2 -1
  50. package/dist/cli/commands/test/runners/vitest.js.map +1 -1
  51. package/dist/cli/utils/bundle.js +1 -1
  52. package/dist/cli/utils/bundle.js.map +1 -1
  53. package/dist/cli/utils/cli-git.d.ts.map +1 -1
  54. package/dist/cli/utils/cli-git.js +11 -8
  55. package/dist/cli/utils/cli-git.js.map +1 -1
  56. package/dist/cli/utils/maybe-create-git-folder.d.ts.map +1 -1
  57. package/dist/cli/utils/maybe-create-git-folder.js +7 -1
  58. package/dist/cli/utils/maybe-create-git-folder.js.map +1 -1
  59. package/dist/utils/get-configuration.d.ts +9 -3
  60. package/dist/utils/get-configuration.d.ts.map +1 -1
  61. package/dist/utils/get-configuration.js.map +1 -1
  62. package/dist/utils/get-jest-config.d.ts.map +1 -1
  63. package/dist/utils/get-jest-config.js +20 -9
  64. package/dist/utils/get-jest-config.js.map +1 -1
  65. package/dist/utils/index.d.ts +1 -0
  66. package/dist/utils/index.d.ts.map +1 -1
  67. package/dist/utils/index.js +1 -0
  68. package/dist/utils/index.js.map +1 -1
  69. package/dist/utils/omit.d.ts +2 -0
  70. package/dist/utils/omit.d.ts.map +1 -0
  71. package/dist/utils/omit.js +28 -0
  72. package/dist/utils/omit.js.map +1 -0
  73. package/package.json +22 -17
  74. package/src/cli/commands/review/rules/__mocks__/mock-config.ts +8 -2
  75. package/src/cli/commands/review/rules/__tests__/no-deprecated-content-base.test.ts +123 -0
  76. package/src/cli/commands/review/rules/__tests__/no-direct-peer-dependencies.test.ts +130 -0
  77. package/src/cli/commands/review/rules/__tests__/prefer-open-ended-peer-dependencies.test.ts +121 -0
  78. package/src/cli/commands/review/rules/__tests__/require-compatible-launch-darkly-sdk.test.ts +256 -0
  79. package/src/cli/commands/review/rules/index.ts +10 -2
  80. package/src/cli/commands/review/rules/no-deprecated-content-base.ts +50 -0
  81. package/src/cli/commands/review/rules/no-direct-peer-dependencies.ts +58 -0
  82. package/src/cli/commands/review/rules/no-typescript-entry-point.ts +5 -8
  83. package/src/cli/commands/review/rules/prefer-open-ended-peer-dependencies.ts +89 -0
  84. package/src/cli/commands/review/rules/require-compatible-launch-darkly-sdk.ts +141 -0
  85. package/src/cli/commands/review/rules/require-explicit-side-effects.ts +14 -14
  86. package/src/cli/commands/review/rules/require-one-collection-version.ts +1 -3
  87. package/src/cli/commands/review/rules/require-project-version-in-root-node-modules.ts +5 -9
  88. package/src/cli/commands/review/rules/require-servicetitan-scope.ts +14 -14
  89. package/src/cli/commands/review/types.ts +16 -2
  90. package/src/cli/commands/review/utils/check-packages.ts +19 -0
  91. package/src/cli/commands/review/utils/index.ts +1 -0
  92. package/src/cli/commands/test/runners/__tests__/vitest.test.ts +82 -13
  93. package/src/cli/commands/test/runners/vitest.ts +4 -2
  94. package/src/cli/utils/__tests__/cli-git.test.ts +20 -18
  95. package/src/cli/utils/__tests__/maybe-create-git-folder.test.ts +1 -1
  96. package/src/cli/utils/bundle.ts +1 -1
  97. package/src/cli/utils/cli-git.ts +8 -7
  98. package/src/cli/utils/maybe-create-git-folder.ts +6 -1
  99. package/src/utils/__tests__/get-jest-config.test.ts +44 -0
  100. package/src/utils/get-configuration.ts +6 -2
  101. package/src/utils/get-jest-config.ts +36 -18
  102. package/src/utils/index.ts +1 -0
  103. package/src/utils/omit.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/startup",
3
- "version": "32.0.1",
3
+ "version": "32.2.0",
4
4
  "description": "",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/startup",
6
6
  "repository": {
@@ -37,8 +37,8 @@
37
37
  "@jest/core": "~29.7.0",
38
38
  "@jest/types": "~29.6.3",
39
39
  "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
40
- "@servicetitan/eslint-config": "32.0.1",
41
- "@servicetitan/stylelint-config": "32.0.1",
40
+ "@servicetitan/eslint-config": "32.2.0",
41
+ "@servicetitan/stylelint-config": "32.2.0",
42
42
  "@svgr/webpack": "^8.1.0",
43
43
  "@swc/cli": "^0.5.0",
44
44
  "@swc/core": "1.13.5",
@@ -52,7 +52,7 @@
52
52
  "debounce": "^2.2.0",
53
53
  "debug": "^4.4.3",
54
54
  "deepmerge": "~4.3.1",
55
- "eslint": "~9.35.0",
55
+ "eslint": "~9.37.0",
56
56
  "execa": "~5.1.1",
57
57
  "glob": "~11.0.3",
58
58
  "html-webpack-plugin": "~5.6.4",
@@ -63,33 +63,34 @@
63
63
  "jest-environment-jsdom": "^29.7.0",
64
64
  "jest-fetch-mock": "~3.0.3",
65
65
  "json5": "^2.2.3",
66
- "lerna": "~8.2.4",
67
- "less": "~4.4.1",
66
+ "jwa": "^1.4.2",
67
+ "lerna": "~9.0.0",
68
+ "less": "~4.4.2",
68
69
  "less-loader": "~12.3.0",
69
70
  "less-plugin-npm-import": "~2.1.0",
70
71
  "lodash.memoize": "^4.1.2",
71
- "memfs": "~4.38.2",
72
+ "memfs": "~4.49.0",
72
73
  "mini-css-extract-plugin": "~2.9.4",
73
74
  "moment-locales-webpack-plugin": "~1.2.0",
74
75
  "multimatch": "~5.0.0",
75
76
  "portfinder": "~1.0.38",
76
77
  "postcss": "~8.5.6",
77
78
  "prettier": "~3.6.2",
78
- "sass": "~1.92.1",
79
+ "sass": "~1.93.2",
79
80
  "sass-loader": "~16.0.5",
80
- "semver": "~7.7.2",
81
+ "semver": "~7.7.3",
81
82
  "source-map-loader": "~5.0.0",
82
83
  "style-loader": "~4.0.0",
83
- "stylelint": "~16.24.0",
84
- "terminal-link": "^4.0.0",
84
+ "stylelint": "~16.25.0",
85
+ "terminal-link": "^5.0.0",
85
86
  "terser-webpack-plugin": "^5.3.14",
86
- "ts-jest": "29.4.1",
87
+ "ts-jest": "29.4.5",
87
88
  "ts-node": "~10.9.2",
88
89
  "typed-css-modules": "~0.9.1",
89
- "typescript": "5.9.2",
90
+ "typescript": "5.9.3",
90
91
  "vitest": "^3.2.4",
91
- "webpack": "~5.101.3",
92
- "webpack-assets-manifest": "~6.2.2",
92
+ "webpack": "~5.102.1",
93
+ "webpack-assets-manifest": "~6.3.0",
93
94
  "webpack-bundle-analyzer": "^4.10.2",
94
95
  "webpack-dev-server": "~5.2.2",
95
96
  "webpack-filter-warnings-plugin": "~1.2.1",
@@ -101,7 +102,7 @@
101
102
  "yargs": "~17.7.2"
102
103
  },
103
104
  "peerDependencies": {
104
- "moment": "^2.30.1"
105
+ "moment": ">=2.30.1"
105
106
  },
106
107
  "peerDependenciesMeta": {
107
108
  "moment": {
@@ -112,6 +113,10 @@
112
113
  "@swc/cli": [
113
114
  "Update when https://github.com/swc-project/swc/issues/10535 is resolved",
114
115
  "See also SwcCompilePackage.execute in file://./src/cli/tasks/swc-compile-package.ts"
116
+ ],
117
+ "jwa": [
118
+ "@progress/kendo-licensing@1.3.5 -> jsonwebtoken@9.0.2 -> jws@3.2.2 -> jwa@1.4.1",
119
+ "Forcing ^1.4.2 to fix Node v25 incompatibility in 1.4.1 (see https://github.com/auth0/node-jsonwebtoken/issues/992)"
115
120
  ]
116
121
  },
117
122
  "publishConfig": {
@@ -120,5 +125,5 @@
120
125
  "cli": {
121
126
  "webpack": false
122
127
  },
123
- "gitHead": "2b74dd72d39c2023151699d1cf7a6e6130d64279"
128
+ "gitHead": "76a83fea33e04809b97c72720751878b1e3002d7"
124
129
  }
@@ -8,7 +8,13 @@ export function mockConfig({
8
8
  }: {
9
9
  id: string;
10
10
  level?: 'error' | 'warn';
11
- exclude: string | string[];
11
+ exclude: string | string[] | Record<string, string[]>;
12
12
  }): ReviewConfiguration {
13
- return { rules: { [id]: exclude ? [level, { exclude: toArray(exclude) }] : [level] } };
13
+ return {
14
+ rules: {
15
+ [id]: exclude
16
+ ? [level, { exclude: typeof exclude === 'object' ? exclude : toArray(exclude) }]
17
+ : [level],
18
+ },
19
+ };
14
20
  }
@@ -0,0 +1,123 @@
1
+ import { execSync } from 'child_process';
2
+ import path from 'path';
3
+ import {
4
+ ErrorSeverity,
5
+ FixCategory,
6
+ Package,
7
+ PackageError,
8
+ ReviewConfiguration,
9
+ } from '../../types';
10
+ import { expectCalls } from '../../__mocks__';
11
+ import { mockConfig, mockProject } from '../__mocks__';
12
+ import { NoDeprecatedContentBase } from '../no-deprecated-content-base';
13
+
14
+ jest.mock('child_process', () => ({ execSync: jest.fn() }));
15
+
16
+ describe(`[startup] Review ${NoDeprecatedContentBase.name}`, () => {
17
+ const id = 'no-deprecated-content-base';
18
+ const rule = new NoDeprecatedContentBase();
19
+ let config: ReviewConfiguration;
20
+ let pkg: Package;
21
+ let packages: Package[];
22
+
23
+ beforeEach(() => {
24
+ config = {};
25
+ pkg = { name: 'lib1', location: path.normalize('packages/lib') };
26
+ packages = [pkg];
27
+ jest.clearAllMocks();
28
+ });
29
+
30
+ const subject = () => rule.run(mockProject({ config, packages }));
31
+
32
+ const fixSubject = () => rule.fix(subject()![0]);
33
+
34
+ function itReturnsError() {
35
+ test('returns error', () => {
36
+ expect(subject()).toEqual([
37
+ expect.objectContaining({
38
+ id,
39
+ message: `package "${pkg.name}" uses deprecated cli.webpack.contentBase configuration`,
40
+ location: pkg.location,
41
+ fixable: FixCategory.isolated,
42
+ severity: ErrorSeverity.warning,
43
+ } satisfies PackageError),
44
+ ]);
45
+ });
46
+ }
47
+
48
+ function itReturnsNothing() {
49
+ test('returns nothing', () => {
50
+ expect(subject()).toEqual([]);
51
+ });
52
+ }
53
+
54
+ itReturnsNothing();
55
+
56
+ describe('when package uses cli.webpack.contentBase', () => {
57
+ const contentBase = 'foo';
58
+
59
+ beforeEach(() => {
60
+ pkg.cli = { webpack: { contentBase } };
61
+ });
62
+
63
+ itReturnsError();
64
+
65
+ test('fixes error', () => {
66
+ fixSubject();
67
+
68
+ expectCalls(
69
+ ...[
70
+ `npm pkg set cli.webpack.static.directory="${contentBase}" -w ${pkg.location}`,
71
+ `npm pkg delete cli.webpack.contentBase -w ${pkg.location}`,
72
+ ].map(command => [execSync, command, { stdio: 'inherit' }])
73
+ );
74
+ });
75
+
76
+ test('fix ignores invalid error', () => {
77
+ rule.fix({} as any);
78
+
79
+ expect(execSync).not.toHaveBeenCalled();
80
+ });
81
+
82
+ describe('when config excludes package', () => {
83
+ beforeEach(() => (config = mockConfig({ id, exclude: pkg.name })));
84
+
85
+ itReturnsNothing();
86
+ });
87
+
88
+ describe('when multiple packages use cli.webpack.contentBase', () => {
89
+ beforeEach(() => {
90
+ packages.push({
91
+ name: 'lib2',
92
+ location: path.normalize('packages/lib2'),
93
+ cli: { webpack: { contentBase: 'bar' } },
94
+ });
95
+ });
96
+
97
+ test('returns multiple errors', () => {
98
+ expect(subject()).toEqual([
99
+ expect.objectContaining({ location: packages[0].location }),
100
+ expect.objectContaining({ location: packages[1].location }),
101
+ ]);
102
+ });
103
+ });
104
+
105
+ describe('when cli.webpack.contentBase is an array', () => {
106
+ const contentBaseArray = ['foo', 'bar'];
107
+
108
+ beforeEach(() => (pkg.cli = { webpack: { contentBase: contentBaseArray } }));
109
+
110
+ test('fixes error', () => {
111
+ fixSubject();
112
+
113
+ expectCalls(
114
+ ...[
115
+ `npm pkg set cli.webpack.static[0].directory="${contentBaseArray[0]}" -w ${pkg.location}`,
116
+ `npm pkg set cli.webpack.static[1].directory="${contentBaseArray[1]}" -w ${pkg.location}`,
117
+ `npm pkg delete cli.webpack.contentBase -w ${pkg.location}`,
118
+ ].map(command => [execSync, command, { stdio: 'inherit' }])
119
+ );
120
+ });
121
+ });
122
+ });
123
+ });
@@ -0,0 +1,130 @@
1
+ import { execSync } from 'child_process';
2
+ import path from 'path';
3
+ import { FixCategory, Package, PackageError, ReviewConfiguration } from '../../types';
4
+ import { expectCalls } from '../../__mocks__';
5
+ import { mockConfig, mockProject } from '../__mocks__';
6
+ import { NoDirectPeerDependencies } from '../no-direct-peer-dependencies';
7
+
8
+ jest.mock('child_process', () => ({ execSync: jest.fn() }));
9
+
10
+ describe(`[startup] Review ${NoDirectPeerDependencies.name}`, () => {
11
+ const id = 'no-direct-peer-dependencies';
12
+ const rule = new NoDirectPeerDependencies();
13
+ let config: ReviewConfiguration;
14
+ let pkg: Package;
15
+ let packages: Package[];
16
+
17
+ beforeEach(() => {
18
+ config = {};
19
+ pkg = { name: 'lib1', location: path.normalize('packages/lib') };
20
+ packages = [pkg];
21
+ jest.clearAllMocks();
22
+ });
23
+
24
+ const subject = () => rule.run(mockProject({ config, packages }));
25
+
26
+ const fixSubject = () => rule.fix(subject()![0]);
27
+
28
+ function itReturnsError(message: () => string) {
29
+ test('returns error', () => {
30
+ expect(subject()).toEqual([
31
+ expect.objectContaining({
32
+ id,
33
+ message: message(),
34
+ location: pkg.location,
35
+ fixable: FixCategory.isolated,
36
+ } satisfies PackageError),
37
+ ]);
38
+ });
39
+ }
40
+
41
+ function itReturnsNothing() {
42
+ test('returns nothing', () => {
43
+ expect(subject()).toEqual([]);
44
+ });
45
+ }
46
+
47
+ itReturnsNothing();
48
+
49
+ describe('when package is both peer and direct dependency', () => {
50
+ beforeEach(() => {
51
+ Object.assign(pkg, {
52
+ dependencies: { foo: '^1.0.0', baz: '1.0.0' },
53
+ peerDependencies: { foo: '>=1.0.0', qux: '^1.0.0' },
54
+ });
55
+ });
56
+
57
+ itReturnsError(() => {
58
+ return `package "${pkg.name}" lists "foo" as both a direct and a peer dependency`;
59
+ });
60
+
61
+ test('fixes error', () => {
62
+ fixSubject();
63
+
64
+ expectCalls(
65
+ ...[
66
+ `npm pkg set devDependencies["foo"]="${pkg.dependencies!.foo}" -w ${pkg.location}`,
67
+ `npm pkg delete dependencies["foo"] -w ${pkg.location}`,
68
+ ].map(command => [execSync, command, { stdio: 'inherit' }])
69
+ );
70
+ });
71
+
72
+ test('fix ignores invalid error', () => {
73
+ rule.fix({} as any);
74
+
75
+ expect(execSync).not.toHaveBeenCalled();
76
+ });
77
+
78
+ describe('when config excludes package', () => {
79
+ beforeEach(() => (config = mockConfig({ id, exclude: pkg.name })));
80
+
81
+ itReturnsNothing();
82
+ });
83
+
84
+ describe('when multiple packages violate the rule', () => {
85
+ beforeEach(() => {
86
+ packages = [
87
+ pkg,
88
+ {
89
+ ...pkg,
90
+ name: 'lib2',
91
+ location: path.normalize('packages/lib2'),
92
+ },
93
+ ];
94
+ });
95
+
96
+ test('returns multiple errors', () => {
97
+ expect(subject()).toEqual([
98
+ expect.objectContaining({ location: packages[0].location }),
99
+ expect.objectContaining({ location: packages[1].location }),
100
+ ]);
101
+ });
102
+ });
103
+ });
104
+
105
+ describe('when multiple packages are peer and direct dependencies', () => {
106
+ beforeEach(() => {
107
+ Object.assign(pkg, {
108
+ dependencies: { foo: '^1.0.0', bar: '~1.0.0', baz: '1.0.0' },
109
+ peerDependencies: { foo: '>= 1.0.0', bar: '^1.0.0', quz: '^1.0.0' },
110
+ });
111
+ });
112
+
113
+ itReturnsError(() => {
114
+ return `package "${pkg.name}" lists "foo" and "bar" as both direct and peer dependencies`;
115
+ });
116
+
117
+ test('fixes multiple errors', () => {
118
+ fixSubject();
119
+
120
+ expectCalls(
121
+ ...[
122
+ `npm pkg set devDependencies["foo"]="${pkg.dependencies!.foo}" -w ${pkg.location}`,
123
+ `npm pkg delete dependencies["foo"] -w ${pkg.location}`,
124
+ `npm pkg set devDependencies["bar"]="${pkg.dependencies!.bar}" -w ${pkg.location}`,
125
+ `npm pkg delete dependencies["bar"] -w ${pkg.location}`,
126
+ ].map(command => [execSync, command, { stdio: 'inherit' }])
127
+ );
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,121 @@
1
+ import { execSync } from 'child_process';
2
+ import { ErrorSeverity, FixCategory, Package, PackageError } from '../../types';
3
+ import { mockConfig, mockProject } from '../__mocks__';
4
+ import { PreferOpenEndedPeerDependencies } from '../prefer-open-ended-peer-dependencies';
5
+
6
+ jest.mock('child_process', () => ({ execSync: jest.fn() }));
7
+
8
+ describe(`[startup] Review ${PreferOpenEndedPeerDependencies.name}`, () => {
9
+ const id = 'prefer-open-ended-peer-dependencies';
10
+ const rule = new PreferOpenEndedPeerDependencies();
11
+ let config: any;
12
+ let pkg: Package;
13
+ let packages: Package[];
14
+
15
+ beforeEach(() => {
16
+ config = {};
17
+ pkg = { name: 'lib1', location: 'packages/lib1' };
18
+ packages = [pkg];
19
+ jest.clearAllMocks();
20
+ });
21
+
22
+ const subject = () => rule.run(mockProject({ config, packages }));
23
+
24
+ const fixSubject = () => rule.fix(subject()![0]);
25
+
26
+ function itReturnsError(message: () => string) {
27
+ test('returns error', () => {
28
+ expect(subject()).toEqual([
29
+ expect.objectContaining({
30
+ id,
31
+ message: message(),
32
+ location: pkg.location,
33
+ severity: ErrorSeverity.warning,
34
+ fixable: FixCategory.isolated,
35
+ } satisfies PackageError),
36
+ ]);
37
+ });
38
+ }
39
+
40
+ function itReturnsNothing() {
41
+ test('returns nothing', () => {
42
+ expect(subject()).toEqual([]);
43
+ });
44
+ }
45
+
46
+ itReturnsNothing();
47
+
48
+ describe('when a package has a closed-ended peer dependency', () => {
49
+ beforeEach(() => (pkg.peerDependencies = { foo: '^12.3.0' }));
50
+
51
+ itReturnsError(
52
+ () => `package "${pkg.name}" has closed-ended peer dependency on "foo@^12.3.0"`
53
+ );
54
+
55
+ test('fixes error', () => {
56
+ fixSubject();
57
+
58
+ expect(execSync).toHaveBeenCalledWith(
59
+ `npm pkg set peerDependencies["foo"]=">=12.3.0" -w ${pkg.location}`,
60
+ { stdio: 'inherit' }
61
+ );
62
+ });
63
+
64
+ test('fix ignores invalid error', () => {
65
+ rule.fix({} as any);
66
+
67
+ expect(execSync).not.toHaveBeenCalled();
68
+ });
69
+
70
+ describe('when config excludes package', () => {
71
+ beforeEach(() => (config = mockConfig({ id, exclude: pkg.name })));
72
+
73
+ itReturnsNothing();
74
+ });
75
+
76
+ describe('when peer is open-ended', () => {
77
+ beforeEach(() => (pkg.peerDependencies!.foo = '>=12.3.0'));
78
+
79
+ itReturnsNothing();
80
+ });
81
+
82
+ describe('when peer dependency is an exact version', () => {
83
+ beforeEach(() => (pkg.peerDependencies!.foo = '12.3.0'));
84
+
85
+ itReturnsNothing();
86
+ });
87
+
88
+ describe('with multiple closed-ended dependencies', () => {
89
+ beforeEach(() => (pkg.peerDependencies!.bar = '~12.3.4'));
90
+
91
+ test('returns multiple errors', () => {
92
+ expect(subject()).toEqual([
93
+ expect.objectContaining({ message: expect.stringContaining('foo@^12.3.0') }),
94
+ expect.objectContaining({ message: expect.stringContaining('bar@~12.3.4') }),
95
+ ]);
96
+ });
97
+
98
+ describe('when dependency is excluded', () => {
99
+ beforeEach(() => (config = mockConfig({ id, exclude: { [pkg.name]: ['foo'] } })));
100
+
101
+ test('omits excluded dependency', () => {
102
+ expect(subject()).toEqual([
103
+ expect.objectContaining({
104
+ message: expect.stringContaining('bar@~12.3.4'),
105
+ }),
106
+ ]);
107
+ });
108
+ });
109
+ });
110
+
111
+ describe.each(['<1', '^1 || ^2', '>1 <2', '1.0 - 2.0'])('when version is "%s"', version => {
112
+ beforeEach(() => (pkg.peerDependencies!.foo = version));
113
+
114
+ test('error is not fixable', () => {
115
+ expect(subject()).not.toEqual([
116
+ expect.objectContaining({ fixable: expect.anything() }),
117
+ ]);
118
+ });
119
+ });
120
+ });
121
+ });