Skip to content

Commit 06f2e88

Browse files
committed
feat(depTypes): handle '!peer' and '**'
1 parent fa85cda commit 06f2e88

40 files changed

+312
-70
lines changed

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module.exports = {
1010
coverageReporters: ['html', 'lcov'],
1111
coverageThreshold: {
1212
global: {
13-
branches: 79,
13+
branches: 77,
1414
functions: 79,
1515
lines: 86,
1616
statements: 85,

site/docs/config/dependency-types.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ properties of package.json files will be inspected by syncpack:
2424
}
2525
```
2626

27+
Negated types are also supported, so here everything **except** `dependencies` and `devDependencies`
28+
will be inspected:
29+
30+
```json title=".syncpackrc"
31+
{
32+
"dependencyTypes": ["!dev", "!prod"]
33+
}
34+
```
35+
2736
:::tip
2837

2938
Syncpack config files also support

site/docs/option/types.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Running syncpack multiple times with different options to target different parts
2828
package.json files is a lot of work to maintain, so [`semverGroups`](../config/semver-groups.mdx)
2929
exist to make this easier.
3030

31-
The above example would would defined like so:
31+
The above example would be defined like so:
3232

3333
```json
3434
{

site/src/partials/version-group-config/dependency-types.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ as its version number, regardless of what versions of the same dependencies migh
3636
}
3737
```
3838

39+
Negated types are also supported, so here everything **except** `dependencies` and `devDependencies`
40+
will be assigned to this group:
41+
42+
```json title="Negated types"
43+
{
44+
"versionGroups": [
45+
{
46+
"packages": ["**"],
47+
"dependencies": ["**"],
48+
"dependencyTypes": ["!dev", "!prod"],
49+
"isIgnored": true
50+
}
51+
]
52+
}
53+
```
54+
3955
:::tip
4056

4157
Syncpack config files also support

src/config/get-enabled-types.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { getEnabledTypes } from './get-enabled-types';
2+
import * as Effect from '@effect/io/Effect';
3+
4+
const prod = expect.objectContaining({ path: 'dependencies' });
5+
const dev = expect.objectContaining({ path: 'devDependencies' });
6+
const local = expect.objectContaining({ path: 'version' });
7+
const overrides = expect.objectContaining({ path: 'overrides' });
8+
const peerDependencies = expect.objectContaining({ path: 'peerDependencies' });
9+
const pnpmOverrides = expect.objectContaining({ path: 'pnpm.overrides' });
10+
const resolutions = expect.objectContaining({ path: 'resolutions' });
11+
// custom
12+
const engines = expect.objectContaining({ path: 'engines' });
13+
14+
it('defaults to all when nothing is provided', () => {
15+
expect(getEnabledTypes({ cli: {}, rcFile: {} })).toEqual(
16+
Effect.succeed([dev, local, overrides, peerDependencies, pnpmOverrides, prod, resolutions]),
17+
);
18+
});
19+
20+
it('uses every type except a negated type such as "!prod"', () => {
21+
expect(getEnabledTypes({ cli: { types: '!prod' }, rcFile: {} })).toEqual(
22+
Effect.succeed([dev, local, overrides, peerDependencies, pnpmOverrides, resolutions]),
23+
);
24+
});
25+
26+
it('handles multiple negated types', () => {
27+
expect(getEnabledTypes({ cli: { types: '!prod,!dev' }, rcFile: {} })).toEqual(
28+
Effect.succeed([local, overrides, peerDependencies, pnpmOverrides, resolutions]),
29+
);
30+
});
31+
32+
it('uses only provided type when defined', () => {
33+
expect(getEnabledTypes({ cli: { types: 'dev' }, rcFile: {} })).toEqual(Effect.succeed([dev]));
34+
});
35+
36+
it('handles multiple types', () => {
37+
expect(getEnabledTypes({ cli: { types: 'dev,peer' }, rcFile: {} })).toEqual(
38+
Effect.succeed([dev, peerDependencies]),
39+
);
40+
});
41+
42+
it('gives precedence to cli options', () => {
43+
expect(getEnabledTypes({ cli: { types: 'dev' }, rcFile: { dependencyTypes: ['peer'] } })).toEqual(
44+
Effect.succeed([dev]),
45+
);
46+
});
47+
48+
it('includes custom types when others are negated', () => {
49+
expect(
50+
getEnabledTypes({
51+
cli: { types: '!dev' },
52+
rcFile: {
53+
customTypes: {
54+
engines: {
55+
path: 'engines',
56+
strategy: 'versionsByName',
57+
},
58+
},
59+
},
60+
}),
61+
).toEqual(
62+
Effect.succeed([local, overrides, peerDependencies, pnpmOverrides, prod, resolutions, engines]),
63+
);
64+
});
65+
66+
it('includes custom types when named', () => {
67+
expect(
68+
getEnabledTypes({
69+
cli: { types: 'dev,engines' },
70+
rcFile: {
71+
customTypes: {
72+
engines: {
73+
path: 'engines',
74+
strategy: 'versionsByName',
75+
},
76+
},
77+
},
78+
}),
79+
).toEqual(Effect.succeed([dev, engines]));
80+
});
81+
82+
it('includes every type when "**" is provided', () => {
83+
expect(
84+
getEnabledTypes({
85+
cli: { types: '**' },
86+
rcFile: {
87+
customTypes: {
88+
engines: {
89+
path: 'engines',
90+
strategy: 'versionsByName',
91+
},
92+
},
93+
},
94+
}),
95+
).toEqual(
96+
Effect.succeed([
97+
dev,
98+
local,
99+
overrides,
100+
peerDependencies,
101+
pnpmOverrides,
102+
prod,
103+
resolutions,
104+
engines,
105+
]),
106+
);
107+
});

src/config/get-enabled-types.ts

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as Data from '@effect/data/Data';
2+
import { isNonEmptyArray } from '@effect/data/ReadonlyArray';
23
import * as Effect from '@effect/io/Effect';
34
import { isArrayOfStrings } from 'tightrope/guard/is-array-of-strings';
45
import { isBoolean } from 'tightrope/guard/is-boolean';
56
import { isEmptyArray } from 'tightrope/guard/is-empty-array';
67
import { isNonEmptyString } from 'tightrope/guard/is-non-empty-string';
7-
import { DEFAULT_CONFIG } from '../constants';
8+
import { INTERNAL_TYPES } from '../constants';
89
import type { Ctx } from '../get-context';
910
import { NameAndVersionPropsStrategy } from '../strategy/name-and-version-props';
1011
import { VersionsByNameStrategy } from '../strategy/versions-by-name';
@@ -19,8 +20,6 @@ export class RenamedWorkspaceTypeError extends Data.TaggedClass('RenamedWorkspac
1920
Record<string, never>
2021
> {}
2122

22-
// @TODO accept `dependencyTypes: ['**']`
23-
// @TODO support `dependencyTypes: ['!dev']`
2423
export function getEnabledTypes({
2524
cli,
2625
rcFile,
@@ -29,55 +28,79 @@ export function getEnabledTypes({
2928
DeprecatedTypesError | RenamedWorkspaceTypeError,
3029
Strategy.Any[]
3130
> {
32-
const enabledTypes: Strategy.Any[] = [];
33-
const enabledTypeNames = (
34-
isNonEmptyString(cli.types)
31+
const deprecatedTypeProps = getDeprecatedTypeProps();
32+
33+
if (deprecatedTypeProps.length > 0) {
34+
return Effect.fail(new DeprecatedTypesError({ types: deprecatedTypeProps }));
35+
}
36+
37+
const allStrategiesByName: Record<string, Strategy.Any> = Object.fromEntries([
38+
['dev', new VersionsByNameStrategy('dev', 'devDependencies')],
39+
['local', new NameAndVersionPropsStrategy('local', 'version', 'name')],
40+
['overrides', new VersionsByNameStrategy('overrides', 'overrides')],
41+
['peer', new VersionsByNameStrategy('peer', 'peerDependencies')],
42+
['pnpmOverrides', new VersionsByNameStrategy('pnpmOverrides', 'pnpm.overrides')],
43+
['prod', new VersionsByNameStrategy('prod', 'dependencies')],
44+
['resolutions', new VersionsByNameStrategy('resolutions', 'resolutions')],
45+
...getCustomTypes({ cli, rcFile }).map((customType) => [customType.name, customType]),
46+
]);
47+
const allStrategyNames = Object.keys(allStrategiesByName);
48+
49+
const names: Record<'provided' | 'enabled' | 'positive' | 'negative', string[]> = {
50+
provided: (isNonEmptyString(cli.types)
3551
? cli.types.split(',')
3652
: isArrayOfStrings(rcFile.dependencyTypes)
3753
? rcFile.dependencyTypes
3854
: []
39-
).filter(isNonEmptyString);
40-
const useDefaults = isEmptyArray(enabledTypeNames);
41-
42-
const deprecatedTypes = DEFAULT_CONFIG.dependencyTypes.filter((key) =>
43-
isBoolean((rcFile as Record<string, boolean>)[key]),
44-
);
55+
).filter(isNonEmptyString),
56+
enabled: [],
57+
positive: [],
58+
negative: [],
59+
};
4560

46-
if (deprecatedTypes.length > 0) {
47-
return Effect.fail(new DeprecatedTypesError({ types: deprecatedTypes }));
61+
if (isEmptyArray(names.provided) || names.provided.join('') === '**') {
62+
return Effect.succeed(allStrategyNames.map(getStrategyByName));
4863
}
4964

50-
if (enabledTypeNames.includes('workspace')) {
51-
return Effect.fail(new RenamedWorkspaceTypeError({}));
52-
}
65+
names.provided.forEach((name) => {
66+
if (name.startsWith('!')) {
67+
names.negative.push(name.replace('!', ''));
68+
} else {
69+
names.positive.push(name);
70+
}
71+
});
5372

54-
if (useDefaults || enabledTypeNames.includes('dev')) {
55-
enabledTypes.push(new VersionsByNameStrategy('dev', 'devDependencies'));
56-
}
57-
if (useDefaults || enabledTypeNames.includes('overrides')) {
58-
enabledTypes.push(new VersionsByNameStrategy('overrides', 'overrides'));
59-
}
60-
if (useDefaults || enabledTypeNames.includes('peer')) {
61-
enabledTypes.push(new VersionsByNameStrategy('peer', 'peerDependencies'));
73+
if (isNonEmptyArray(names.negative)) {
74+
allStrategyNames.forEach((name) => {
75+
if (!names.negative.includes(name)) {
76+
names.enabled.push(name);
77+
}
78+
});
6279
}
63-
if (useDefaults || enabledTypeNames.includes('pnpmOverrides')) {
64-
enabledTypes.push(new VersionsByNameStrategy('pnpmOverrides', 'pnpm.overrides'));
65-
}
66-
if (useDefaults || enabledTypeNames.includes('prod')) {
67-
enabledTypes.push(new VersionsByNameStrategy('prod', 'dependencies'));
68-
}
69-
if (useDefaults || enabledTypeNames.includes('resolutions')) {
70-
enabledTypes.push(new VersionsByNameStrategy('resolutions', 'resolutions'));
80+
81+
if (isNonEmptyArray(names.positive)) {
82+
names.positive.forEach((name) => {
83+
if (!names.enabled.includes(name)) {
84+
names.enabled.push(name);
85+
}
86+
});
7187
}
72-
if (useDefaults || enabledTypeNames.includes('local')) {
73-
enabledTypes.push(new NameAndVersionPropsStrategy('localPackage', 'version', 'name'));
88+
89+
if (names.enabled.includes('workspace')) {
90+
return Effect.fail(new RenamedWorkspaceTypeError({}));
7491
}
7592

76-
getCustomTypes({ cli, rcFile }).forEach((customType) => {
77-
if (useDefaults || enabledTypeNames.includes(customType.name)) {
78-
enabledTypes.push(customType);
79-
}
80-
});
93+
return Effect.succeed(names.enabled.map(getStrategyByName));
8194

82-
return Effect.succeed(enabledTypes);
95+
function getStrategyByName(type: string): Strategy.Any {
96+
return allStrategiesByName[type] as Strategy.Any;
97+
}
98+
99+
/**
100+
* Look for dependency types defined using the old syntax of `{ prod: true }`
101+
* which was deprecated in [email protected].
102+
*/
103+
function getDeprecatedTypeProps() {
104+
return INTERNAL_TYPES.filter((key) => isBoolean((rcFile as Record<string, boolean>)[key]));
105+
}
83106
}

src/constants.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,18 @@ export const RANGE = {
2424
WORKSPACE: 'workspace:',
2525
} as const;
2626

27+
export const INTERNAL_TYPES = [
28+
'dev',
29+
'local',
30+
'overrides',
31+
'peer',
32+
'pnpmOverrides',
33+
'prod',
34+
'resolutions',
35+
] as const;
36+
2737
export const DEFAULT_CONFIG = {
28-
dependencyTypes: ['dev', 'local', 'overrides', 'peer', 'pnpmOverrides', 'prod', 'resolutions'],
38+
dependencyTypes: ['**'],
2939
filter: '.',
3040
indent: ' ',
3141
semverGroups: [],

src/get-semver-groups/filtered-out.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class FilteredOutSemverGroup extends Data.TaggedClass('FilteredOut')<{
1515
super({
1616
config: {
1717
dependencies: ['**'],
18-
dependencyTypes: [],
18+
dependencyTypes: ['**'],
1919
label: 'Filtered out',
2020
packages: ['**'],
2121
},

src/get-semver-groups/with-range.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class WithRangeSemverGroup extends Data.TaggedClass('WithRange')<{
4141
);
4242
}
4343

44-
const isLocalPackageInstance = instance.strategy.name === 'localPackage';
44+
const isLocalPackageInstance = instance.strategy.name === 'local';
4545
const exactVersion = setSemverRange('', instance.specifier);
4646
const expectedVersion = setSemverRange(this.config.range, instance.specifier);
4747

src/get-version-groups/filtered-out.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class FilteredOutVersionGroup extends Data.TaggedClass('FilteredOut')<{
1616
super({
1717
config: {
1818
dependencies: ['**'],
19-
dependencyTypes: [],
19+
dependencyTypes: ['**'],
2020
label: 'Filtered out',
2121
packages: ['**'],
2222
},

0 commit comments

Comments
 (0)