Skip to content

Commit

Permalink
Merge pull request #9 from NullVoxPopuli/support-sub-paths-configs
Browse files Browse the repository at this point in the history
Support `overrides` for specified paths (as globs) and varying ranges for devDependencies and dependencies, separately
  • Loading branch information
NullVoxPopuli authored Jan 28, 2024
2 parents cc99d25 + d16acd1 commit e5f4a0f
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 48 deletions.
33 changes: 26 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,32 @@ Example `.defragrc.yaml`
# to an exact version.
#
# Possible values:
# pinned
# patches
# minors
write-as: pinned
# Default assumes every package follows semver and
# is 100% safe to upgrade within semver ranges.
# pinned ( default )
# patches ( e.g.: ~1.0.0 )
# minors ( e.g.: ^1.0.0 )
write-as: pinned
# This whole object is optional,
# but if your monorepo has public libraries
# where you *need* wider ranges, that can be configured here
#
# overrides is an array of objects
overrides:
# path may be either a single glob or an array of glabs
- path:
- packages/*/addon/package.json
- packages/*/*/package.json
# for both devDependencies and dependencies
# in addition to the 3 global options,
# pinned, patches, and minors, you may also
# specify `false` to disable version re-writing entirely
# (which may be useful for some packages that do
# complex multi-version specifiers)
devDependencies: pinned
dependencies: minors

# Default assumes every package follows semver (minors) and
# is 100% safe to upgrade within those ranges.
#
# in practice, this is only true if all dependencies
# sharing those listed here do not use private APIs.
Expand Down
16 changes: 5 additions & 11 deletions src/-tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import type { Config } from '../types.js';
import { normalizeConfig } from '../config.js';

export function c(overrides: Partial<Config> = {}): Config {
return {
'write-as': 'pinned' as const,
...overrides,
'update-range': {
'~': [],
'^': [],
...overrides['update-range'],
},
};
import type { Config, UserConfig } from '../types.js';

export function c(userConfig: UserConfig = {}): Config {
return normalizeConfig(userConfig);
}
54 changes: 54 additions & 0 deletions src/config-getOverride.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect as e, it } from 'vitest';

import { c } from './-tests/helpers.ts';
import { getOverride } from './config.js';

const expect = e.soft;

describe('getOverride', () => {
it('resolves a package glob', () => {
expect(
getOverride(
'packages/ember-repl/addon',
c({
overrides: [
{
path: [
'packages/*/addon/package.json',
'packages/syntax/*/package.json',
],
dependencies: false,
devDependencies: 'pinned',
},
],
}),
),
).toMatchInlineSnapshot(`
{
"dependencies": false,
"devDependencies": "pinned",
"path": [
"packages/*/addon/package.json",
"packages/syntax/*/package.json",
],
}
`);
});

it('returns nothing when no match is found', () => {
expect(
getOverride(
'apps/repl',
c({
overrides: [
{
path: ['packages/*/addon/package.json'],
dependencies: false,
devDependencies: 'pinned',
},
],
}),
),
).toBe(undefined);
});
});
88 changes: 88 additions & 0 deletions src/config-normalizeConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect as e, it } from 'vitest';

import { normalizeConfig } from './config.js';

const expect = e.soft;

describe('normalizeConfig', () => {
it('no passed config', () => {
expect(normalizeConfig()).toMatchInlineSnapshot(`
{
"overrides": [],
"update-range": {
"^": [],
"~": [],
},
"write-as": "pinned",
}
`);
});

it('overrides "write-as"', () => {
expect(normalizeConfig({ 'write-as': 'minors' })).toMatchInlineSnapshot(`
{
"overrides": [],
"update-range": {
"^": [],
"~": [],
},
"write-as": "minors",
}
`);
});

it('specifies overrides', () => {
expect(
normalizeConfig({
overrides: [
{
path: 'x/y/z',
devDependencies: false,
dependencies: false,
},
],
}),
).toMatchInlineSnapshot(`
{
"overrides": [
{
"dependencies": false,
"devDependencies": false,
"path": [
"x/y/z",
],
},
],
"update-range": {
"^": [],
"~": [],
},
"write-as": "pinned",
}
`);
});

it('specifies update-range', () => {
expect(
normalizeConfig({
'update-range': {
'~': ['ember-data'],
'^': ['@ember-data/*'],
},
}),
).toMatchInlineSnapshot(`
{
"overrides": [],
"update-range": {
"^": [
"@ember-data/*",
],
"~": [
"ember-data",
],
},
"write-as": "pinned",
}
`);
});
});
56 changes: 56 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import path from 'node:path';

import { minimatch } from 'minimatch';

/**
* @param {Partial<import('./types.ts').UserConfig>} [ userConfig ]
* @return {import('./types.ts').Config}
*/
export function normalizeConfig(userConfig) {
let config = userConfig || {};

let topLevel = {
'write-as': config['write-as'] || 'pinned',
};

/** @type {import('./types.ts').Config['overrides'] } */
const overrides =
config['overrides']?.map((override) => {
const defaultRange = topLevel['write-as'];
const pathsArray = Array.isArray(override.path)
? override.path
: [override.path];

return {
devDependencies: override['devDependencies'] ?? defaultRange,
dependencies: override['dependencies'] ?? defaultRange,
path: pathsArray,
};
}) || [];

return {
...topLevel,
'update-range': {
'~': [],
'^': [],
...config['update-range'],
},
overrides,
};
}

/**
* @param {string} relativePath the workspace path
* @param {import('./types.ts').Config} config
*/
export function getOverride(relativePath, config) {
let { overrides } = config;

let packageJsonPath = path.join(relativePath, 'package.json');

let override = overrides.find((override) => {
return override.path.some((match) => minimatch(packageJsonPath, match));
});

return override;
}
74 changes: 50 additions & 24 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { cosmiconfig } from 'cosmiconfig';
import debug from 'debug';
import { packageJson, project } from 'ember-apply';

import { getOverride, normalizeConfig } from './config.js';
import { injestDeps, updateManifestFor } from './utils.js';

const d = debug('defrag');
Expand All @@ -20,45 +21,70 @@ export default async function run() {
const projectResult = await getPackages(root);

/**
* @type {import('./types.ts').Config}
* @type {import('./types.ts').UserConfig}
*/
const config = {
'write-as': 'pinned',
...(configResult?.config || {}),
};
const userConfig = configResult?.config || {};
const config = normalizeConfig(userConfig);

d(`Resolved config:`);
d(config);

let manifests = [
projectResult.rootPackage?.packageJson,
...projectResult.packages.map((p) => p.packageJson),
].filter(Boolean);
let packages = [projectResult.rootPackage, ...projectResult.packages].filter(
Boolean,
);

d(`Found ${manifests.length} packages`);
manifests.forEach((p) => p && injestDeps(p));
d(`Found ${packages.length} packages`);
packages.forEach((p) => p && injestDeps(p.packageJson));

let paths = [
projectResult.rootPackage?.dir,
...projectResult.packages.map((p) => p.dir),
].filter(Boolean);
for (const pkg of packages) {
if (!pkg) continue;

d(`Found ${paths.length} paths`);

for (let projectPath of paths) {
if (!projectPath) continue;

d(`Updating ${projectPath.replace(root, '')}`);
d(`Updating ${pkg.relativeDir}`);

await packageJson.modify((manifest) => {
updateManifestFor(manifest.devDependencies, config);
updateManifestFor(manifest.dependencies, config);
// These have configurable overrides in .defragrc.yml
update(manifest, pkg.relativeDir, config, 'devDependencies');
update(manifest, pkg.relativeDir, config, 'dependencies');

// These don't have configurable overrides as they
// *are* the overrides for the whole repo
// (or in some cases a single workspace)
// In any case, they only affect local development,
// and not versions used by a consumer in a published package.
//
// npm
updateManifestFor(manifest.overrides, config);
// yarn
updateManifestFor(manifest.resolutions, config);
// pnpm
updateManifestFor(manifest.pnpm?.overrides, config);
}, projectPath);
}, pkg.dir);
}
}

/**
* @param {Record<string, any>} manifest
* @param {string} relativePath
* @param {import('./types.ts').Config} config
* @param {'devDependencies' | 'dependencies'} collection
*/
function update(manifest, relativePath, config, collection) {
let override = getOverride(relativePath, config);

if (!override) {
return updateManifestFor(manifest[collection], config);
}

let collectionConfig = override[collection];

if (collectionConfig === false) {
return;
}

let writeAs = collectionConfig ?? config['write-as'];

updateManifestFor(manifest[collection], {
'write-as': writeAs,
'update-range': config['update-range'],
});
}
24 changes: 23 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,34 @@ import type { Package } from '@manypkg/get-packages';

export type Manifest = Package['packageJson'];

type Range = 'pinned' | 'minors' | 'patches';

export type ConfigForUpdate = Pick<Config, 'write-as' | 'update-range'>;

export interface Config {
'write-as': 'pinned' | 'minors' | 'patches';
'write-as': Range;
'update-range': {
// list of names or globs to match packages against
'~': string[];
'^': string[];
// '>=': string[]
};
overrides: {
path: string[];
devDependencies: Range | false;
dependencies: Range | false;
}[];
}

export interface UserConfig {
'write-as'?: Range;
'update-range'?: {
'~'?: string[];
'^'?: string[];
};
overrides?: {
path: string | string[];
devDependencies?: Range | false;
dependencies?: Range | false;
}[];
}
Loading

0 comments on commit e5f4a0f

Please sign in to comment.