Skip to content

Commit

Permalink
fix(version): add missing lifecycle code from lerna
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Mar 4, 2022
1 parent 5bfdc5d commit a0d9e95
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 7 deletions.
19 changes: 19 additions & 0 deletions packages/core/src/__mocks__/utils/run-lifecycle.js
@@ -0,0 +1,19 @@
"use strict";

const mockRunLifecycle = jest.fn((pkg) => Promise.resolve(pkg));
const mockCreateRunner = jest.fn((opts) => (pkg, stage) => {
// no longer the actual API, but approximates inner logic of default export
if (pkg.scripts[stage]) {
return mockRunLifecycle(pkg, stage, opts);
}

return Promise.resolve();
});

function getOrderedCalls() {
return mockRunLifecycle.mock.calls.map(([pkg, script]) => [pkg.name, script]);
}

module.exports.runLifecycle = mockRunLifecycle;
module.exports.createRunner = mockCreateRunner;
module.exports.runLifecycle.getOrderedCalls = getOrderedCalls;
2 changes: 1 addition & 1 deletion packages/publish/src/__tests__/npm-publish.test.js
Expand Up @@ -7,7 +7,7 @@ jest.mock("fs-extra");
jest.mock('@lerna-lite/core', () => ({
...jest.requireActual('@lerna-lite/core'), // return the other real methods, below we'll mock only 2 of the methods
otplease: (cb, opts) => Promise.resolve(cb(opts)),
runLifecycle: jest.requireActual('../../../core/src/__mocks__/run-lifecycle').runLifecycle,
runLifecycle: jest.requireActual('../../../core/src/__mocks__/utils/run-lifecycle').runLifecycle,
}));

// mocked modules
Expand Down
2 changes: 1 addition & 1 deletion packages/publish/src/publishCommand.ts
Expand Up @@ -149,7 +149,7 @@ export class PublishCommand extends Command {
}

// a 'rooted leaf' is the regrettable pattern of adding '.' to the 'packages' config in lerna.json
this.hasRootedLeaf = this.packageGraph?.has(this.project?.manifest.name) ?? false;
this.hasRootedLeaf = this.packageGraph.has(this.project.manifest.name);

if (this.hasRootedLeaf) {
this.logger.info('publish', 'rooted leaf detected, skipping synthetic root lifecycles');
Expand Down
133 changes: 133 additions & 0 deletions packages/version/src/__tests__/version-lifecycle-scripts.spec.ts
@@ -0,0 +1,133 @@
"use strict";

// local modules _must_ be explicitly mocked
jest.mock("../lib/git-push", () => jest.requireActual('../lib/__mocks__/git-push'));
jest.mock("../lib/is-anything-committed", () => jest.requireActual('../lib/__mocks__/is-anything-committed'));
jest.mock("../lib/is-behind-upstream", () => jest.requireActual('../lib/__mocks__/is-behind-upstream'));
jest.mock("../lib/remote-branch-exists", () => jest.requireActual('../lib/__mocks__/remote-branch-exists'));

jest.mock('@lerna-lite/core', () => ({
...jest.requireActual('@lerna-lite/core') as any, // return the other real methods, below we'll mock only 2 of the methods
logOutput: jest.requireActual('../../../core/src/__mocks__/output').logOutput,
promptConfirmation: jest.requireActual('../../../core/src/__mocks__/prompt').promptConfirmation,
promptSelectOne: jest.requireActual('../../../core/src/__mocks__/prompt').promptSelectOne,
promptTextInput: jest.requireActual('../../../core/src/__mocks__/prompt').promptTextInput,
createRunner: jest.requireActual('../../../core/src/__mocks__/utils/run-lifecycle').createRunner,
runLifecycle: jest.requireActual('../../../core/src/__mocks__/utils/run-lifecycle').runLifecycle,
throwIfUncommitted: jest.requireActual('../../../core/src/__mocks__/check-working-tree').throwIfUncommitted,
}));

const yargParser = require('yargs-parser');
import { runLifecycle } from '@lerna-lite/core';
const loadJsonFile = require("load-json-file");
import 'dotenv/config';

// helpers
const initFixture = require("../../../../helpers/init-fixture")(__dirname);

// test command
import { VersionCommand } from '../versionCommand';

const createArgv = (cwd: string, ...args: string[]) => {
args.unshift('version');
const parserArgs = args.map(String);
const argv = yargParser(parserArgs);
argv['$0'] = cwd;
return argv;
};

describe("lifecycle scripts", () => {
const npmLifecycleEvent = process.env.npm_lifecycle_event;

afterEach(() => {
process.env.npm_lifecycle_event = npmLifecycleEvent;
});

it("calls version lifecycle scripts for root and packages", async () => {
const cwd = await initFixture("lifecycle");

await new VersionCommand(createArgv(cwd));

expect(runLifecycle).toHaveBeenCalledTimes(6);

["preversion", "version", "postversion"].forEach((script) => {
// "lifecycle" is the root manifest name
expect(runLifecycle).toHaveBeenCalledWith(
expect.objectContaining({ name: "lifecycle" }),
script,
expect.any(Object)
);
expect(runLifecycle).toHaveBeenCalledWith(
expect.objectContaining({ name: "package-1" }),
script,
expect.any(Object)
);
});

// package-2 lacks version lifecycle scripts
expect(runLifecycle).not.toHaveBeenCalledWith(
expect.objectContaining({ name: "package-2" }),
expect.any(String)
);

expect((runLifecycle as any).getOrderedCalls()).toEqual([
["lifecycle", "preversion"],
["package-1", "preversion"],
["package-1", "version"],
["lifecycle", "version"],
["package-1", "postversion"],
["lifecycle", "postversion"],
]);

expect(Array.from(loadJsonFile.registry.keys())).toStrictEqual([
"/packages/package-1",
"/packages/package-2",
"/" // TODO: investigate why the original Lerna doesn't have this extra one in the root
]);
});

it("does not execute recursive root scripts", async () => {
const cwd = await initFixture("lifecycle");

process.env.npm_lifecycle_event = "version";

await new VersionCommand(createArgv(cwd));

expect((runLifecycle as any).getOrderedCalls()).toEqual([
["package-1", "preversion"],
["package-1", "version"],
["package-1", "postversion"],
]);
});

it("does not duplicate rooted leaf scripts", async () => {
const cwd = await initFixture("lifecycle-rooted-leaf");

await new VersionCommand(createArgv(cwd));

expect((runLifecycle as any).getOrderedCalls()).toEqual([
["package-1", "preversion"],
["package-1", "version"],
["lifecycle-rooted-leaf", "preversion"],
["lifecycle-rooted-leaf", "version"],
["lifecycle-rooted-leaf", "postversion"],
["package-1", "postversion"],
]);
});

it("respects --ignore-scripts", async () => {
const cwd = await initFixture("lifecycle");

await new VersionCommand(createArgv(cwd, "--ignore-scripts"));

// despite all the scripts being passed to runLifecycle()
// none of them will actually execute as long as opts["ignore-scripts"] is provided
expect(runLifecycle).toHaveBeenCalledWith(
expect.objectContaining({ name: "lifecycle" }),
"version",
expect.objectContaining({
"ignore-scripts": true,
})
);
});
});
13 changes: 8 additions & 5 deletions packages/version/src/versionCommand.ts
Expand Up @@ -56,7 +56,7 @@ export class VersionCommand extends Command {
releaseNotes: ReleaseNote[] = [];
gitOpts: any;
runPackageLifecycle: any;
runRootLifecycle!: (stage: string) => Promise<void>;
runRootLifecycle!: (stage: string) => Promise<void> | void;
savePrefix = '';
tags: string[] = [];
updates: any[] = [];
Expand Down Expand Up @@ -247,17 +247,20 @@ export class VersionCommand extends Command {
}

// a "rooted leaf" is the regrettable pattern of adding "." to the "packages" config in lerna.json
this.hasRootedLeaf = this.packageGraph.has(this.options.packages);
this.hasRootedLeaf = this.packageGraph.has(this.project.manifest.name);

if (this.hasRootedLeaf && !this.composed) {
this.logger.info('version', 'rooted leaf detected, skipping synthetic root lifecycles');
}

this.runPackageLifecycle = createRunner(this.options);

this.runRootLifecycle = (stage) => {
return this.runPackageLifecycle(this.project, stage);
};
// don't execute recursively if run from a poorly-named script
this.runRootLifecycle = /^(pre|post)?version$/.test(process.env.npm_lifecycle_event as string)
? (stage) => {
this.logger.warn('lifecycle', 'Skipping root %j because it has already been called', stage);
}
: (stage) => this.runPackageLifecycle(this.project.manifest, stage);

// amending a commit probably means the working tree is dirty
if (this.commitAndTag && this.gitOpts.amend !== true) {
Expand Down

0 comments on commit a0d9e95

Please sign in to comment.