Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dependenciesCustomPaths options #113

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/bin-fix-mismatches/fix-mismatches.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,19 @@ describe('fixMismatches', () => {
scenario.files['packages/c/package.json'].logEntryWhenChanged,
]);
});

it('fix version in dependenciesCustomPath using the highest installed version', () => {
const scenario = scenarios.customDepPath();
fixMismatchesCli(scenario.config, scenario.disk);
expect(scenario.disk.writeFileSync.mock.calls).toEqual([
scenario.files['packages/b/package.json'].diskWriteWhenChanged,
scenario.files['packages/c/package.json'].diskWriteWhenChanged,
]);
expect(scenario.log.mock.calls).toEqual([
scenario.files['packages/a/package.json'].logEntryWhenUnchanged,
scenario.files['packages/b/package.json'].logEntryWhenChanged,
scenario.files['packages/c/package.json'].logEntryWhenChanged,
]);
});
});
});
28 changes: 27 additions & 1 deletion src/bin-fix-mismatches/fix-mismatches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@ export function fixMismatches(ctx: Context): Context {
if (instanceGroup.hasMismatches && !instanceGroup.isIgnored) {
const nextVersion = instanceGroup.getExpectedVersion();
instanceGroup.instances.forEach(
({ dependencyType, version, packageJsonFile }) => {
({
dependencyType,
dependencyCustomPath,
version,
packageJsonFile,
}) => {
const root: any = packageJsonFile.contents;
if (version !== nextVersion) {
if (dependencyType === 'pnpmOverrides') {
root.pnpm.overrides[instanceGroup.name] = nextVersion;
} else if (dependencyType === 'customDependencies') {
updateObjectNestedKeyFromPath(
root,
dependencyCustomPath as string,
nextVersion,
);
} else {
root[dependencyType][instanceGroup.name] = nextVersion;
}
Expand All @@ -44,3 +55,18 @@ export function fixMismatches(ctx: Context): Context {

return ctx;
}

function updateObjectNestedKeyFromPath(
obj: any,
nestedPropertyPath: string,
value: string | undefined,
) {
const properties = nestedPropertyPath.split('.');
const lastKeyIndex = properties.length - 1;
for (let i = 0; i < lastKeyIndex; ++i) {
const key = properties[i];
if (!(key in obj)) obj[key] = {};
obj = obj[key];
}
obj[properties[lastKeyIndex]] = value;
}
Comment on lines +59 to +72
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should probably not be here but in a utils file somewhere, but I didn't know where to put it, so I let it here for now, waiting for instructions.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! yeah something like this will definitely be needed.

We have props('path.t.a.value', isNumber)(someObject) (tests here) for getting but nothing for setting. If you'd like to make this that would be great.

I'm using https://mobily.github.io/ts-belt/api/result a lot which I appreciate is a style of coding that may not be to everyone's taste, so if you don't want to that's fine too.

Thanks a lot for this PR and your work so far, it's unlucky timing that it came right when I was doing a huge refactor. Things will be settled again now.

14 changes: 12 additions & 2 deletions src/bin-lint-semver-ranges/lint-semver-ranges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ export function lintSemverRanges(ctx: Context): Context {
}

mismatches.forEach(
({ dependencyType, name, version, packageJsonFile }) => {
({
dependencyType,
dependencyCustomPath,
name,
version,
packageJsonFile,
}) => {
const loc =
dependencyType === 'customDependencies'
? `"${dependencyCustomPath}"`
: dependencyType;
console.log(
chalk`{red ✕ ${name}} {red.dim ${version} in ${dependencyType} of ${
chalk`{red ✕ ${name}} {red.dim ${version} in ${loc} of ${
packageJsonFile.contents.name
} should be ${setSemverRange(semverGroup.range, version)}}`,
);
Expand Down
20 changes: 20 additions & 0 deletions src/bin-list-mismatches/list-mismatches-cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,25 @@ describe('listMismatches', () => {
[` 0.1.0 in dependencies of ${normalize(c)}`],
]);
});

it('warns about the mismatch in customDependencies version', () => {
const scenario = scenarios.customDepPath();
const a = 'packages/a/package.json';
const b = 'packages/b/package.json';
const c = 'packages/c/package.json';
const customPath = scenario.config.dependenciesCustomPaths?.find(
(dep) => (dep.name = 'foo'),
)?.path;
listMismatchesCli(scenario.config, scenario.disk);
expect(scenario.log.mock.calls).toEqual(
[
[`- foo: 0.2.0 is the highest valid semver version in use`],
[` 0.2.0 in dependencies of ${normalize(a)}`],
[` 0.1.0 in devDependencies of ${normalize(b)}`],
[` 0.0.1 in "${customPath}" of ${normalize(c)}`],
].map(([msg]) => [normalize(msg)]),
);
expect(scenario.disk.process.exit).toHaveBeenCalledWith(1);
});
});
});
13 changes: 11 additions & 2 deletions src/bin-list-mismatches/list-mismatches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,20 @@ export function listMismatches(ctx: Context): Context {
}

instanceGroup.instances.forEach(
({ dependencyType, version, packageJsonFile }) => {
({
dependencyType,
dependencyCustomPath,
version,
packageJsonFile,
}) => {
const isMatch = version === expected;
const isLocal = dependencyType === 'workspace';
const shortPath = relative(CWD, packageJsonFile.filePath);
const loc = isLocal ? 'version' : dependencyType;
const loc = isLocal
? 'version'
: dependencyType === 'customDependencies'
? `"${dependencyCustomPath}"`
: dependencyType;
if (isMatch) {
console.log(chalk`{green ${version} in ${loc} of ${shortPath}}`);
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const ALL_DEPENDENCY_TYPES = [
'pnpmOverrides',
'resolutions',
'workspace',
'customDependencies',
] as const;

export const RANGE = {
Expand Down Expand Up @@ -64,4 +65,5 @@ export const DEFAULT_CONFIG: Config.RcFile = {
sortFirst: ['name', 'description', 'version', 'author'],
source: [],
versionGroups: [],
dependenciesCustomPaths: [],
};
9 changes: 9 additions & 0 deletions src/lib/get-context/get-config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export namespace Config {
}
}

/**
* Custom property path within package.json files where versions can be found
*/
export type DependencyCustomPath = { name: string; path: string };

/** All valid config which can only be provided via .syncpackrc */
interface RcFileOnly {
/**
Expand All @@ -86,6 +91,10 @@ export namespace Config {
versionGroups: VersionGroup.Any[];
/** */
semverGroups: SemverGroup.Any[];
/** Custom path in the package.json that point to a dependencies
* @example {name: 'foo', path: 'x:y.z'}
*/
dependenciesCustomPaths: DependencyCustomPath[];
}

/** All valid config which can only be provided via the CLI */
Expand Down
22 changes: 21 additions & 1 deletion src/lib/get-context/get-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export const getConfig = (
? Boolean(program.workspace)
: getOption<boolean>('workspace', isBoolean);

const dependenciesCustomPaths = getOption<Config.DependencyCustomPath[]>(
'dependenciesCustomPaths',
isArrayOfDependencyCustomPath,
);
const customDependencies = dependenciesCustomPaths?.length > 0;

const dependencyTypes =
dev ||
overrides ||
Expand All @@ -75,7 +81,8 @@ export const getConfig = (
(type === 'pnpmOverrides' && pnpmOverrides) ||
(type === 'dependencies' && prod) ||
(type === 'resolutions' && resolutions) ||
(type === 'workspace' && workspace),
(type === 'workspace' && workspace) ||
(type === 'customDependencies' && customDependencies),
)
: [...ALL_DEPENDENCY_TYPES];

Expand Down Expand Up @@ -117,6 +124,7 @@ export const getConfig = (

const finalConfig: InternalConfig = {
dev,
dependenciesCustomPaths,
filter,
indent,
workspace,
Expand Down Expand Up @@ -177,4 +185,16 @@ export const getConfig = (
)
);
}

function isArrayOfDependencyCustomPath(
value: unknown,
): value is Config.DependencyCustomPath[] {
return (
isArray(value) &&
value.every(
(value: unknown) =>
isObject(value) && isString(value.name) && isString(value.path),
)
);
}
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { isNonEmptyString } from 'expect-more';
import type { Disk } from '../../../disk';
import { isSemver } from '../../../is-semver';
import { verbose } from '../../../log';
import { newlines } from '../../../newlines';
import type { DependencyType } from '../../get-config/config';
import type { Config, DependencyType } from '../../get-config/config';
import type { InternalConfig } from '../../get-config/internal-config';
import type { JsonFile } from '../get-patterns/read-json-safe';
import { Instance } from './instance';
Expand Down Expand Up @@ -76,11 +77,29 @@ export class PackageJsonFile {
}

getInstances(): Instance[] {
const dependencyCustomPathByName =
this.program.dependenciesCustomPaths.reduce<Record<string, string>>(
(depPathByName, customDep) => {
depPathByName[customDep.name] = customDep.path;
return depPathByName;
},
{},
);
return this.program.dependencyTypes
.flatMap((dependencyType): Instance[] =>
this.getDependencyEntries(dependencyType, this.contents).map(
this.getDependencyEntries(
dependencyType,
this.contents,
this.program.dependenciesCustomPaths,
).map(
([name, version]) =>
new Instance(dependencyType, name, this, version),
new Instance(
dependencyType,
name,
this,
version,
dependencyCustomPathByName[name],
),
),
)
.filter((instance) => {
Expand All @@ -105,6 +124,7 @@ export class PackageJsonFile {
getDependencyEntries(
dependencyType: DependencyType,
contents: PackageJson,
dependenciesCustomPaths: Config.DependencyCustomPath[],
): [string, string][] {
switch (dependencyType) {
case 'dependencies':
Expand All @@ -120,6 +140,34 @@ export class PackageJsonFile {
case 'pnpmOverrides': {
return Object.entries(contents?.pnpm?.overrides || {});
}
case 'customDependencies': {
return this.getCustomDependenciesEntries(
contents,
dependenciesCustomPaths,
);
}
}
}

getCustomDependenciesEntries(
contents: PackageJson,
dependenciesCustomPaths: Config.DependencyCustomPath[],
): [string, string][] {
return dependenciesCustomPaths.map((dependencyCustomPath) =>
this.getCustomDependencyEntries(contents, dependencyCustomPath),
);
}

getCustomDependencyEntries(
contents: PackageJson,
dependencyCustomPath: Config.DependencyCustomPath,
): [string, string] {
const properties = dependencyCustomPath.path.split('.');
// That's bit bad looking but reduce is works really well for the job here
const version = properties.reduce<Record<string, unknown>>(
(prev, curr) => prev && (prev[curr] as Record<string, unknown>),
contents,
) as unknown as string;
return [dependencyCustomPath.name, isSemver(version) ? version : ''];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { Config, DependencyType } from '../../get-config/config';
export class Instance {
/** where this dependency is installed */
dependencyType: DependencyType;
/** if dependencyType is "customDependencies" his associated dependencyCustomPath */
dependencyCustomPath?: string;
/** the name of this dependency */
name: string;
/** The package this dependency is installed in this specific time */
Expand All @@ -20,8 +22,10 @@ export class Instance {
name: string,
packageJsonFile: PackageJsonFile,
version: string,
dependencyCustomPath?: string,
) {
this.dependencyType = dependencyType;
this.dependencyCustomPath = dependencyCustomPath;
this.name = name;
this.packageJsonFile = packageJsonFile;
this.pkgName = packageJsonFile.contents.name || 'PACKAGE_JSON_HAS_NO_NAME';
Expand Down
44 changes: 44 additions & 0 deletions test/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,4 +648,48 @@ export const scenarios = {
},
);
},

/**
* A, B & C depend on foo
* C has foo at a dependenciesCustomPath 'x:y.z'
* The versions mismatch
* `0.2.0` is the highest valid semver version
* All packages should be fixed to use `0.2.0`
*/
customDepPath() {
return createScenario(
[
{
path: 'packages/a/package.json',
before: mockPackage('a', { deps: ['foo@0.2.0'] }),
after: mockPackage('a', { deps: ['foo@0.2.0'] }),
},
{
path: 'packages/b/package.json',
before: mockPackage('b', { devDeps: ['foo@0.1.0'] }),
after: mockPackage('b', { devDeps: ['foo@0.2.0'] }),
},
{
path: 'packages/c/package.json',
before: mockPackage('c', {
otherProps: {
'x:y': {
z: '0.0.1',
},
},
}),
after: mockPackage('c', {
otherProps: {
'x:y': {
z: '0.2.0',
},
},
}),
},
],
{
dependenciesCustomPaths: [{ name: 'foo', path: 'x:y.z' }],
},
);
},
};