Skip to content

Commit

Permalink
fix(publish): use Lerna code for detectFromGit and detectFromPackage
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Mar 6, 2022
1 parent f787999 commit 811111f
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Expand Up @@ -6,7 +6,7 @@ charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
insert_final_newline = false
trim_trailing_whitespace = true

[*]
Expand Down
148 changes: 148 additions & 0 deletions packages/publish/src/__tests__/publish-from-git.test.js
@@ -0,0 +1,148 @@
"use strict";

// FIXME: better mock for version command
jest.mock("../../../version/src/lib/git-push", () => jest.requireActual("../../../version/src/lib/__mocks__/git-push"));
jest.mock("../../../version/src/lib/is-anything-committed", () => jest.requireActual("../../../version/src/lib/__mocks__/is-anything-committed"));
jest.mock("../../../version/src/lib/is-behind-upstream", () => jest.requireActual("../../../version/src/lib/__mocks__/is-behind-upstream"));
jest.mock("../../../version/src/lib/remote-branch-exists", () => jest.requireActual("../../../version/src/lib/__mocks__/remote-branch-exists"));

// mocked modules, mock only 2 methods from core
jest.mock('@lerna-lite/core', () => ({
...jest.requireActual('@lerna-lite/core'), // return the other real methods, below we'll mock only 2 of the methods
collectUpdates: jest.requireActual('../../../core/src/__mocks__/utils/collect-updates').collectUpdates,
getOneTimePassword: () => Promise.resolve("654321"),
logOutput: jest.requireActual('../../../core/src/__mocks__/output').logOutput,
promptConfirmation: jest.requireActual("../../../core/src/__mocks__/prompt").promptConfirmation,
throwIfUncommitted: jest.requireActual('../../../core/src/__mocks__/check-working-tree').throwIfUncommitted,
}));

// local modules _must_ be explicitly mocked
jest.mock("../lib/get-packages-without-license", () => jest.requireActual('../lib/__mocks__/get-packages-without-license'));
jest.mock("../lib/verify-npm-package-access", () => jest.requireActual('../lib/__mocks__/verify-npm-package-access'));
jest.mock("../lib/get-npm-username", () => jest.requireActual('../lib/__mocks__/get-npm-username'));
jest.mock("../lib/get-two-factor-auth-required", () => jest.requireActual('../lib/__mocks__/get-two-factor-auth-required'));
jest.mock("../lib/get-unpublished-packages", () => jest.requireActual('../lib/__mocks__/get-unpublished-packages'));
jest.mock("../lib/npm-publish", () => jest.requireActual('../lib/__mocks__/npm-publish'));

// mocked modules
const { npmPublish } = require("../lib/npm-publish");
const { logOutput, promptConfirmation, throwIfUncommitted } = require("@lerna-lite/core");

// helpers
const initFixture = require("../../../../helpers/init-fixture")(__dirname);
const { gitTag } = require("../../../../helpers/git-tag");
const { loggingOutput } = require("../../../../helpers/logging-output");

// test command
const yargParser = require('yargs-parser');
const { PublishCommand } = require("../publishCommand");

const createArgv = (cwd, ...args) => {
args.unshift('publish');
const parserArgs = args.join(' ');
const argv = yargParser(parserArgs);
argv['$0'] = cwd;
return argv;
};

describe("publish from-git", () => {
it("publishes tagged packages", async () => {
const cwd = await initFixture("normal");

await gitTag(cwd, "v1.0.0");
await new PublishCommand(createArgv(cwd, '--bump', 'from-git'));

// called from chained describeRef()
expect(throwIfUncommitted).toHaveBeenCalled();

expect(promptConfirmation).toHaveBeenLastCalledWith("Are you sure you want to publish these packages?");
expect(logOutput.logged()).toMatch("Found 4 packages to publish:");
expect(npmPublish.order()).toEqual([
"package-1",
"package-3",
"package-4",
"package-2",
// package-5 is private
]);
});

it("publishes tagged independent packages", async () => {
const cwd = await initFixture("independent");

await Promise.all([
gitTag(cwd, "package-1@1.0.0"),
gitTag(cwd, "package-2@2.0.0"),
gitTag(cwd, "package-3@3.0.0"),
gitTag(cwd, "package-4@4.0.0"),
gitTag(cwd, "package-5@5.0.0"),
]);
await new PublishCommand(createArgv(cwd, '--bump', 'from-git'));

expect(npmPublish.order()).toEqual([
"package-1",
"package-3",
"package-4",
"package-2",
// package-5 is private
]);
});

it("publishes packages matching custom --tag-version-prefix", async () => {
const cwd = await initFixture("normal");

await gitTag(cwd, "foo/1.0.0");
await new PublishCommand(createArgv(cwd, "--bump", "from-git", "--tag-version-prefix", "foo/"));

expect(npmPublish.order()).toEqual([
"package-1",
"package-3",
"package-4",
"package-2",
// package-5 is private
]);
});

it("only publishes independent packages with matching tags", async () => {
const cwd = await initFixture("independent");

await gitTag(cwd, "package-3@3.0.0");
await new PublishCommand(createArgv(cwd, "--bump", "from-git"));

expect(logOutput.logged()).toMatch("Found 1 package to publish:");
expect(npmPublish.order()).toEqual(["package-3"]);
});

it("exits early when the current commit is not tagged", async () => {
const cwd = await initFixture("normal");

await new PublishCommand(createArgv(cwd, "--bump", "from-git"));

expect(npmPublish).not.toHaveBeenCalled();

const logMessages = loggingOutput("info");
expect(logMessages).toContain("No tagged release found. You might not have fetched tags.");
});

it("throws an error when uncommitted changes are present", async () => {
throwIfUncommitted.mockImplementationOnce(() => {
throw new Error("uncommitted");
});

const cwd = await initFixture("normal");
const command = new PublishCommand(createArgv(cwd, "--bump", "from-git"));

await expect(command).rejects.toThrow("uncommitted");
// notably different than the actual message, but good enough here
});

it("throws an error when --git-head is passed", async () => {
const cwd = await initFixture("normal");
const command = new PublishCommand(createArgv(cwd, "--bump", "from-git", "--git-head", "deadbeef"));

await expect(command).rejects.toThrow(
expect.objectContaining({
prefix: "EGITHEAD",
})
);
});
});
139 changes: 139 additions & 0 deletions packages/publish/src/__tests__/publish-from-package.test.js
@@ -0,0 +1,139 @@
"use strict";

// FIXME: better mock for version command
jest.mock("../../../version/src/lib/git-push", () => jest.requireActual("../../../version/src/lib/__mocks__/git-push"));
jest.mock("../../../version/src/lib/is-anything-committed", () => jest.requireActual("../../../version/src/lib/__mocks__/is-anything-committed"));
jest.mock("../../../version/src/lib/is-behind-upstream", () => jest.requireActual("../../../version/src/lib/__mocks__/is-behind-upstream"));
jest.mock("../../../version/src/lib/remote-branch-exists", () => jest.requireActual("../../../version/src/lib/__mocks__/remote-branch-exists"));

// mocked modules, mock only certain methods from core
jest.mock('@lerna-lite/core', () => ({
...jest.requireActual('@lerna-lite/core'), // return the other real methods, below we'll mock only 2 of the methods
collectUpdates: jest.requireActual('../../../core/src/__mocks__/utils/collect-updates').collectUpdates,
getOneTimePassword: () => Promise.resolve("654321"),
logOutput: jest.requireActual('../../../core/src/__mocks__/output').logOutput,
promptConfirmation: jest.requireActual("../../../core/src/__mocks__/prompt").promptConfirmation,
throwIfUncommitted: jest.requireActual('../../../core/src/__mocks__/check-working-tree').throwIfUncommitted,
}));

// local modules _must_ be explicitly mocked
jest.mock("../lib/get-packages-without-license", () => jest.requireActual('../lib/__mocks__/get-packages-without-license'));
jest.mock("../lib/verify-npm-package-access", () => jest.requireActual('../lib/__mocks__/verify-npm-package-access'));
jest.mock("../lib/get-npm-username", () => jest.requireActual('../lib/__mocks__/get-npm-username'));
jest.mock("../lib/get-two-factor-auth-required", () => jest.requireActual('../lib/__mocks__/get-two-factor-auth-required'));
jest.mock("../lib/get-unpublished-packages", () => jest.requireActual('../lib/__mocks__/get-unpublished-packages'));
jest.mock("../lib/pack-directory", () => jest.requireActual('../lib/__mocks__/pack-directory'));
jest.mock("../lib/npm-publish", () => jest.requireActual('../lib/__mocks__/npm-publish'));

const fs = require("fs-extra");
const path = require("path");

// mocked or stubbed modules
const writePkg = require("write-pkg");
const { npmPublish } = require("../lib/npm-publish");
const { logOutput, promptConfirmation, throwIfUncommitted } = require("@lerna-lite/core");
// const { throwIfUncommitted } = require("@lerna/check-working-tree");
const { getUnpublishedPackages } = require("../lib/get-unpublished-packages");

// helpers
const initFixture = require("../../../../helpers/init-fixture")(__dirname);
const { loggingOutput } = require("../../../../helpers/logging-output");

// file under test
const yargParser = require('yargs-parser');
const { PublishCommand } = require("../publishCommand");

const createArgv = (cwd, ...args) => {
args.unshift('publish');
const parserArgs = args.join(' ');
const argv = yargParser(parserArgs);
argv['$0'] = cwd;
return argv;
};

describe("publish from-package", () => {
it("publishes unpublished packages", async () => {
const cwd = await initFixture("normal");

getUnpublishedPackages.mockImplementationOnce((packageGraph) => {
const pkgs = packageGraph.rawPackageList.slice(1, 3);
return pkgs.map((pkg) => packageGraph.get(pkg.name));
});

await new PublishCommand(createArgv(cwd, "--bump", "from-package"));

expect(promptConfirmation).toHaveBeenLastCalledWith("Are you sure you want to publish these packages?");
expect(logOutput.logged()).toMatch("Found 2 packages to publish:");
expect(npmPublish.order()).toEqual(["package-2", "package-3"]);
});

it("publishes unpublished independent packages", async () => {
const cwd = await initFixture("independent");

getUnpublishedPackages.mockImplementationOnce((packageGraph) => Array.from(packageGraph.values()));

await new PublishCommand(createArgv(cwd, "--bump", "from-package"));

expect(npmPublish.order()).toEqual([
"package-1",
"package-3",
"package-4",
"package-2",
// package-5 is private
]);
});

it("exits early when all packages are published", async () => {
const cwd = await initFixture("normal");

await new PublishCommand(createArgv(cwd, "--bump", "from-package"));

expect(npmPublish).not.toHaveBeenCalled();

const logMessages = loggingOutput("notice");
expect(logMessages).toContain("No unpublished release found");
});

it("throws an error when uncommitted changes are present", async () => {
throwIfUncommitted.mockImplementationOnce(() => {
throw new Error("uncommitted");
});

const cwd = await initFixture("normal");
const command = new PublishCommand(createArgv(cwd, "--bump", "from-package"));

await expect(command).rejects.toThrow("uncommitted");
// notably different than the actual message, but good enough here
});

it("does not require a git repo", async () => {
getUnpublishedPackages.mockImplementationOnce((packageGraph) => [packageGraph.get("package-1")]);

const cwd = await initFixture("independent");

// nuke the git repo first
await fs.remove(path.join(cwd, ".git"));
await new PublishCommand(createArgv(cwd, "--bump", "from-package"));

expect(npmPublish).toHaveBeenCalled();
expect(writePkg.updatedManifest("package-1")).not.toHaveProperty("gitHead");

const logMessages = loggingOutput("notice");
expect(logMessages).toContain("Unable to verify working tree, proceed at your own risk");
expect(logMessages).toContain(
"Unable to set temporary gitHead property, it will be missing from registry metadata"
);
expect(logMessages).toContain("Unable to reset working tree changes, this probably isn't a git repo.");
});

it("accepts --git-head override", async () => {
getUnpublishedPackages.mockImplementationOnce((packageGraph) => [packageGraph.get("package-1")]);

const cwd = await initFixture("independent");

await new PublishCommand(createArgv(cwd, "--bump", "from-package", "--git-head", "deadbeef"));

expect(npmPublish).toHaveBeenCalled();
expect(writePkg.updatedManifest("package-1").gitHead).toBe("deadbeef");
});
});
3 changes: 2 additions & 1 deletion packages/publish/src/lib/get-unpublished-packages.ts
@@ -1,3 +1,4 @@
import { FetchConfig, PackageGraph, PackageGraphNode } from '@lerna-lite/core';
import log from 'npmlog';
import pMap from 'p-map';
import pacote from 'pacote';
Expand All @@ -8,7 +9,7 @@ import pacote from 'pacote';
* @param {import("./fetch-config").FetchConfig} opts
* @returns {Promise<import("@lerna/package-graph").PackageGraphNode[]>}
*/
export function getUnpublishedPackages(packageGraph, opts): Promise<any> {
export function getUnpublishedPackages(packageGraph: PackageGraph, opts: FetchConfig): Promise<PackageGraphNode> {
log.silly('getUnpublishedPackages', '');

let chain: Promise<any> = Promise.resolve();
Expand Down

0 comments on commit 811111f

Please sign in to comment.