Skip to content

Commit

Permalink
feat(publish): add new option --remove-package-fields before publish (
Browse files Browse the repository at this point in the history
#359)

* feat(publish): add new option `--remove-package-fields` before publish
  • Loading branch information
ghiscoding committed Oct 14, 2022
1 parent efaf011 commit 45a2107
Show file tree
Hide file tree
Showing 21 changed files with 497 additions and 52 deletions.
6 changes: 6 additions & 0 deletions lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
"loglevel": "verbose",
"npmClient": "pnpm",
"command": {
"publish": {
"removePackageFields": [
"devDependencies",
"scripts"
]
},
"version": {
"conventionalCommits": true,
"createRelease": "github",
Expand Down
4 changes: 2 additions & 2 deletions packages/changed/src/__tests__/changed-command.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jest.mock('@lerna-lite/core', () => ({
}));

// mocked modules
import { collectUpdates, logOutput } from '@lerna-lite/core';
import { ChangedCommandOption, collectUpdates, logOutput } from '@lerna-lite/core';

// helpers
import { commandRunner, initFixtureFactory } from '@lerna-test/helpers';
Expand All @@ -32,7 +32,7 @@ const createArgv = (cwd: string, ...args: string[]) => {
const argv = yargParser(parserArgs, { array: [{ key: 'ignoreChanges' }] });
argv['$0'] = cwd;
argv['loglevel'] = 'silent';
return argv;
return argv as unknown as ChangedCommandOption;
};

// remove quotes around top-level strings
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/schemas/lerna-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,9 @@
"registry": {
"$ref": "#/$defs/commandOptions/shared/registry"
},
"removePackageFields": {
"$ref": "#/$defs/commandOptions/publish/removePackageFields"
},
"requireScripts": {
"$ref": "#/$defs/commandOptions/publish/requireScripts"
},
Expand Down Expand Up @@ -842,6 +845,9 @@
"otp": {
"$ref": "#/$defs/commandOptions/publish/otp"
},
"removePackageFields": {
"$ref": "#/$defs/commandOptions/publish/removePackageFields"
},
"requireScripts": {
"$ref": "#/$defs/commandOptions/publish/requireScripts"
},
Expand Down Expand Up @@ -1215,6 +1221,13 @@
"type": "string",
"description": "During `lerna publish`, supply a one-time password for publishing with two-factor authentication."
},
"removePackageFields": {
"type": "array",
"items": {
"type": "string"
},
"description": "Remove fields from each package.json before publishing them to the registry, removing fields from a complex object is also supported via the dot notation (ie 'scripts.build')"
},
"requireScripts": {
"type": "boolean",
"description": "During `lerna publish`, when true, execute ./scripts/prepublish.js and ./scripts/postpublish.js, relative to package root."
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/cli-commands/cli-publish-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export default {
type: 'string',
requiresArg: true,
},
'remove-package-fields': {
describe:
'Remove fields from each package.json before publishing them to the registry, removing fields from a complex object is also supported via the dot notation (ie "scripts.build").',
type: 'array',
},
'require-scripts': {
describe: 'Execute ./scripts/prepublish.js and ./scripts/postpublish.js, relative to package root.',
type: 'boolean',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jest.mock('../../git-clients', () => ({
parseGitRepo: jest.requireActual('../../__mocks__/github-client').parseGitRepo,
}));

import { getDescendantObjectProp, getGithubCommits } from '../get-github-commits';
import { getGithubCommits } from '../get-github-commits';

const execOpts = { cwd: '/test' };

Expand All @@ -29,35 +29,3 @@ describe('getGithubCommits method', () => {
]);
});
});

describe('getDescendantObjectProp method', () => {
let obj = {};
beforeEach(() => {
obj = { id: 1, user: { firstName: 'John', lastName: 'Doe', address: { number: 123, street: 'Broadway' } } };
});

it('should return original object when no path is provided', () => {
const output = getDescendantObjectProp(obj, undefined);
expect(output).toBe(obj);
});

it('should return undefined when search argument is not part of the input object', () => {
const output = getDescendantObjectProp(obj, 'users');
expect(output).toBe(undefined as any);
});

it('should return the object descendant even when path given is not a dot notation', () => {
const output = getDescendantObjectProp(obj, 'user');
expect(output).toEqual(obj['user']);
});

it('should return the object descendant when using dot notation', () => {
const output = getDescendantObjectProp(obj, 'user.firstName');
expect(output).toEqual('John');
});

it('should return the object descendant when using multiple levels of dot notation', () => {
const output = getDescendantObjectProp(obj, 'user.address.street');
expect(output).toEqual('Broadway');
});
});
18 changes: 2 additions & 16 deletions packages/core/src/conventional-commits/get-github-commits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import log from 'npmlog';

import { createGitHubClient, parseGitRepo } from '../git-clients';
import { ExecOpts, RemoteCommit } from '../models';
import { getComplexObjectValue } from '../utils';

const QUERY_PAGE_SIZE = 100; // GitHub API is restricting max of 100 per query

Expand Down Expand Up @@ -49,7 +50,7 @@ export async function getGithubCommits(
since: sinceDate,
});

const historyData = getDescendantObjectProp<GraphqlCommitHistoryData>(response, 'repository.ref.target.history');
const historyData = getComplexObjectValue<GraphqlCommitHistoryData>(response, 'repository.ref.target.history');
const pageInfo = historyData?.pageInfo;
hasNextPage = pageInfo?.hasNextPage ?? false;
afterCursor = pageInfo?.endCursor ?? '';
Expand All @@ -73,21 +74,6 @@ export async function getGithubCommits(
return remoteCommits;
}

/**
* From a dot (.) notation path, find and return a property within an object given a complex object path
* Note that the object path does should not include the parent itself
* for example if we want to get `address.zip` from `user` object, we would call `getDescendantObjectProp(user, 'address.zip')`
* @param object - object to search from
* @param path - complex object path to find descendant property from, must be a string with dot (.) notation
* @returns outputValue - the object property value found if any
*/
export function getDescendantObjectProp<T>(object: any, path: string | undefined): T {
if (!object || !path) {
return object;
}
return path.split('.').reduce((obj, prop) => obj && (obj as any)[prop], object);
}

interface GraphqlCommitClientData {
repository?: {
ref?: {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/models/command-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ export interface PublishCommandOption extends VersionCommandOption {
/** Use the specified registry for all npm client operations. */
registry?: string;

/** Remove fields from each package.json before publishing them to the registry, removing fields from a complex object is also supported via the dot notation (ie "scripts.build") */
removePackageFields?: string[];

/** Execute ./scripts/prepublish.js and ./scripts/postpublish.js, relative to package root. */
requireScripts?: boolean;

Expand Down
79 changes: 79 additions & 0 deletions packages/core/src/utils/__tests__/object-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import cloneDeep from 'clone-deep';
import npmlog from 'npmlog';

import { deleteComplexObjectProp, getComplexObjectValue } from '../object-utils';

describe('deleteComplexObjectProp method', () => {
let obj = {};
beforeEach(() => {
obj = { id: 1, user: { firstName: 'John', lastName: 'Doe', address: { number: 123, street: 'Broadway' } } };
});

it('should expect the same object as the original object when no path is provided', () => {
const originalObj = cloneDeep(obj);
deleteComplexObjectProp(obj, undefined as any);
expect(originalObj).toEqual(obj);
});

it('should expect the same object as the original object when search argument is not part of the input object', () => {
const originalObj = cloneDeep(obj);
deleteComplexObjectProp(obj, 'users');
expect(originalObj).toEqual(obj as any);
});

it('should expect the object to remove an entire property when path is a single string without dot notation', () => {
const logSpy = jest.spyOn(npmlog, 'verbose');
deleteComplexObjectProp(obj, 'user', 'some object name');
expect(obj).toEqual({ id: 1 });
expect(logSpy).toHaveBeenCalledWith('mutation', 'Removed "user" field from some object name.');
});

it('should expect the object descendant to be removed when path is using dot notation', () => {
const logSpy = jest.spyOn(npmlog, 'verbose');
deleteComplexObjectProp(obj, 'user.firstName');
expect(obj).toEqual({ id: 1, user: { lastName: 'Doe', address: { number: 123, street: 'Broadway' } } });
expect(logSpy).toHaveBeenCalledWith('mutation', 'Removed "user.firstName" field from n/a.');
});

it('should expect the object last descendant to be removed when using multiple levels of dot notation', () => {
const logSpy = jest.spyOn(npmlog, 'verbose');
deleteComplexObjectProp(obj, 'user.address.street', '"@workspace/pkg-1" package');
expect(obj).toEqual({ id: 1, user: { firstName: 'John', lastName: 'Doe', address: { number: 123 } } });
expect(logSpy).toHaveBeenCalledWith(
'mutation',
'Removed "user.address.street" field from "@workspace/pkg-1" package.'
);
});
});

describe('getComplexObjectValue method', () => {
let obj = {};
beforeEach(() => {
obj = { id: 1, user: { firstName: 'John', lastName: 'Doe', address: { number: 123, street: 'Broadway' } } };
});

it('should return original object when no path is provided', () => {
const output = getComplexObjectValue(obj, undefined as any);
expect(output).toBe(obj);
});

it('should return undefined when search argument is not part of the input object', () => {
const output = getComplexObjectValue(obj, 'users');
expect(output).toBe(undefined as any);
});

it('should return the object descendant even when path given is not a dot notation', () => {
const output = getComplexObjectValue(obj, 'user');
expect(output).toEqual(obj['user']);
});

it('should return the object descendant when using dot notation', () => {
const output = getComplexObjectValue(obj, 'user.firstName');
expect(output).toEqual('John');
});

it('should return the object descendant when using multiple levels of dot notation', () => {
const output = getComplexObjectValue(obj, 'user.address.street');
expect(output).toEqual('Broadway');
});
});
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './env-replace';
export * from './find-prefix';
export * from './log-package-error';
export * from './npm-conf';
export * from './object-utils';
export * from './output';
export * from './parse-field';
export * from './prerelease-id-from-version';
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/utils/object-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import log from 'npmlog';

/**
* From a dot (.) notation path, find and delete a property within an object if found given a complex object path
* @param {Object} object - object to search from
* @param {String} path - complex object path to find descendant property from, must be a string with dot (.) notation
* @param {String} [sourceName] - source name of which object name to delete the field from.
*/
export function deleteComplexObjectProp(object: any, path: string, sourceName?: string) {
if (!object || !path) {
return object;
}
const props = path.split('.');
const lastProp = props.slice(-1).pop();

return props.reduce((obj, prop) => {
if (lastProp !== undefined && obj?.[prop] !== undefined && prop === lastProp) {
delete obj[prop];
log.verbose('mutation', `Removed "${path}" field from ${sourceName || 'n/a'}.`);
} else {
return obj?.[prop];
}
}, object);
}

/**
* From a dot (.) notation path, find and return a property within an object given a complex object path
* Note that the object path does should not include the parent itself
* for example if we want to get `address.zip` from `user` object, we would call `getComplexObjectValue(user, 'address.zip')`
* @param object - object to search from
* @param path - complex object path to find descendant property from, must be a string with dot (.) notation
* @returns outputValue - the object property value found if any
*/
export function getComplexObjectValue<T>(object: any, path: string): T {
if (!object || !path) {
return object;
}
return path.split('.').reduce((obj, prop) => obj?.[prop], object);
}
39 changes: 39 additions & 0 deletions packages/publish/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ This is useful when a previous `lerna publish` failed to publish all packages to
- [`--otp`](#--otp)
- [`--preid`](#--preid)
- [`--pre-dist-tag <tag>`](#--pre-dist-tag-tag)
- [`--remove-package-fields <fields>`](#--remove-package-fields-fields) (new)
- [`--registry <url>`](#--registry-url)
- [`--tag-version-prefix`](#--tag-version-prefix)
- [`--temp-tag`](#--temp-tag)
Expand Down Expand Up @@ -268,6 +269,44 @@ lerna publish --pre-dist-tag next

Works the same as [`--dist-tag`](#--dist-tag-tag), except only applies to packages being released with a prerelease version.

### `--remove-package-fields <fields>`

Remove certain fields from every package before publishing them to the registry, we can also remove fields from a complex object structure via the dot notation (ie "scripts.build"). In summary this option is helpful in cleaning each "package.json" of every packages, it allows us to remove any extra fields that do not have any usage outside of the project itself (for example "devDependencies", "scripts", ...).

```sh
# remove "devDepencies" and "scripts" fields from all packages
lerna version --remove-package-fields 'devDependencies' 'scripts'
```

Removal of complex object value(s) are also supported via the dot notation as shown below.

```sh
lerna version --remove-package-fields 'scripts.build'
```

##### output

```diff
{
script: {
- "build": "tsc --project tsconfig.json",
"build:dev": "tsc --incremental --watch"
}
}
```

This option is probably best specified in `lerna.json` configuration

```json
{
"command": {
"publish": {
"removePackageFields": ["devDependencies", "scripts"]
}
}
}
```

### `--registry <url>`

When run with this flag, forwarded npm commands will use the specified registry for your package(s).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "snake-graph",
"description": "when a change in the head (package-1) occurs, the tail (package-5) should be bumped as well"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "package-1",
"description": "no local dependencies, four local dependents (three transitive)",
"version": "1.0.0",
"browser": "src/index.ts",
"main": "dist/cjs/index.js",
"scripts": {
"build": "tsc --project tsconfig.json"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "package-2",
"description": "one local dependency, one direct dependent, no transitive dependencies",
"version": "1.0.0",
"main": "dist/cjs/index.js",
"dependencies": {
"package-1": "^1.0.0"
},
"scripts": {
"build": "tsc --project tsconfig.json",
"build:dev": "tsc --incremental --watch",
"pack-tarball": "npm pack"
}
}

0 comments on commit 45a2107

Please sign in to comment.