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(plugins/npm): add support for passing publishFolder. #2115

Merged
merged 1 commit into from
Mar 4, 2022
Merged
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
43 changes: 43 additions & 0 deletions plugins/npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,49 @@ Set `$NPM_TOKEN` to the "Base64 version of username:password".
}
```

### publishFolder

When publishing packages from a folder other than the project root the `publishFolder` config can be set.

When used with `npm` generates command similar to `npm publish <publishFolder> ...` more info can be found here: [npm-publish](https://docs.npmjs.com/cli/v8/commands/npm-publish).

When used with `lerna` in a monorepo, this functionality implements the `--contents <publishFolder>` flag, more info can be found here: [lerna publish --contents](https://github.com/lerna/lerna/tree/main/commands/publish#--contents-dir).

> :exclamation: **NOTE**: This functionality should be combined with a valid npm [Life Cycle](https://docs.npmjs.com/cli/v8/using-npm/scripts#life-cycle-scripts) script,
> otherwise the `version` of the produced package will not align with the calculated/expected version generated by `auto`.
> See [Life Cycle Scripts](#life-cycle-scripts) for additional info.

```json
{
"plugins": [
[
"npm",
{
"publishFolder": "dist/some-dir"
}
]
]
}
```

#### Life Cycle Scripts

During the publish routine, this plugin will execute (`npm version`|`npx lerna version`), and then immediately follow-up with (`npm publish`|`npx lerna publish`). If you're attempting to publish a package from a directory other than the project root, its likely you're doing some sort of repository or package.json manipulation in an attempt to cleanup (remove devDependencies, scripts, fields) or otherwise manually construct your published package. Since this process is likely invoked prior to the `version` and `publish` event enacted by this plugin, the `version` in your processed `package.json` would not match the new `version` the plugin is attempting to publish (unless you've accounted for this in some other fashion). This simply means we need to insert ourselves between the `version` and `publish` stages of this plugin and re-process or otherwise update the `package.json` file in our `publishFolder`.

Appropriate life cycle scripts (which operate after `version` but before `publish`) would include [`postversion`, `prepublishOnly`, or `prepack`]. Its worth noting the `postversion` life cycle script will be enacted in the context of your root `package.json` file, while `prepublishOnly` and `prepack` would be enacted in the context of your `publishFolder` `package.json` file.

Example life cycle script which is triggered after (`npm version`|`npx lerna version`) and before (`npm publish`|`npx lerna publish`):

```
{
"scripts": {
"postversion": "cd <publishFolder> && npm version $npm_package_version --no-git-tag-version --no-commit-hooks --ignore-scripts",
}
}
```

> :warning: Warning! Use caution when pairing long-running life cyle scripts with auto. [Read more here.](https://intuit.github.io/auto/docs/welcome/quick-merge#beware-long-publishes)

### canaryScope

Publishing canary versions comes with some security risks.
Expand Down
132 changes: 131 additions & 1 deletion plugins/npm/__tests__/npm-next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,61 @@ describe("next", () => {
]);
});

test("works with publishFolder", async () => {
mockFs({
"package.json": `
{
"name": "test",
"version": "1.2.4-next.0"
}
`,
});
const plugin = new NPMPlugin({ publishFolder: "dist/publish-folder" });
const hooks = makeHooks();

plugin.apply(({
config: { prereleaseBranches: ["next"] },
hooks,
remote: "origin",
baseBranch: "main",
logger: dummyLog(),
getCurrentVersion: () => "1.2.3",
prefixRelease: (v: string) => v,
git: {
getLatestRelease: () => "1.0.0",
getLastTagNotInBaseBranch: () => "1.2.3",
},
} as unknown) as Auto.Auto);

expect(
await hooks.next.promise([], { bump: Auto.SEMVER.patch } as any)
).toStrictEqual(["1.2.4-next.0"]);

expect(execPromise).toHaveBeenCalledWith("npm", [
"version",
"1.2.4-next.0",
"--no-git-tag-version",
]);
expect(execPromise).toHaveBeenCalledWith("git", [
"tag",
"1.2.4-next.0",
"-m",
'"Update version to 1.2.4-next.0"',
]);
expect(execPromise).toHaveBeenCalledWith("git", [
"push",
"origin",
"next",
"--tags",
]);
expect(execPromise).toHaveBeenCalledWith("npm", [
"publish",
"dist/publish-folder",
"--tag",
"next",
]);
});

test("works in monorepo", async () => {
mockFs({
"package.json": `
Expand Down Expand Up @@ -464,6 +519,81 @@ describe("next", () => {
]);
});

test("works in monorepo - publishFolder", async () => {
mockFs({
"package.json": `
{
"name": "test",
"version": "1.2.3"
}
`,
"lerna.json": `
{
"version": "1.2.3"
}
`,
});
const plugin = new NPMPlugin({ publishFolder: "dist/publish-folder" });
const hooks = makeHooks();

// isMonorepo
monorepoPackages.mockReturnValueOnce([
{
path: "packages/a",
name: "@packages/a",
package: { version: "1.2.3" },
},
{
path: "packages/b",
name: "@packages/b",
package: { version: "1.2.4-next.0" },
},
]);
execPromise.mockResolvedValueOnce("");
execPromise.mockResolvedValueOnce("");
execPromise.mockResolvedValueOnce("1.2.4-next.0");

plugin.apply(({
config: { prereleaseBranches: ["next"] },
hooks,
remote: "origin",
baseBranch: "main",
logger: dummyLog(),
getCurrentVersion: () => "1.2.3",
prefixRelease: (v: string) => v,
git: {
getLatestRelease: () => "1.0.0",
getLastTagNotInBaseBranch: () => "1.2.3",
},
} as unknown) as Auto.Auto);

expect(
await hooks.next.promise([], { bump: Auto.SEMVER.patch } as any)
).toStrictEqual(["1.2.4-next.0"]);

expect(execPromise).toHaveBeenCalledWith(
"npx",
expect.arrayContaining([
"lerna",
"publish",
"1.2.4-next.0",
"--contents",
"dist/publish-folder"
])
);
expect(execPromise).toHaveBeenCalledWith("git", [
"reset",
"--hard",
"HEAD~1",
]);
expect(execPromise).toHaveBeenCalledWith("git", [
"push",
"origin",
"next",
"--tags",
]);
});

test("works in monorepo - independent", async () => {
mockFs({
"package.json": `
Expand Down Expand Up @@ -690,4 +820,4 @@ describe("next", () => {
await hooks.next.promise([], { bump: Auto.SEMVER.patch } as any)
).toStrictEqual(["@foo/foo@1.0.0-next.0"]);
});
});
});
142 changes: 142 additions & 0 deletions plugins/npm/__tests__/npm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,30 @@ describe("publish", () => {
]);
});

test("monorepo - should use publishFolder", async () => {
const plugin = new NPMPlugin({ publishFolder: "dist/publish-folder" });
const hooks = makeHooks();

plugin.apply({
config: { prereleaseBranches: ["next"] },
hooks,
remote: "origin",
baseBranch: "main",
logger: dummyLog(),
} as Auto.Auto);

await hooks.publish.promise({ bump: Auto.SEMVER.patch });
expect(execPromise).toHaveBeenCalledWith("npx", [
"lerna",
"publish",
"--yes",
"from-package",
false,
"--contents",
"dist/publish-folder",
]);
});

test("should use legacy", async () => {
mockFs({
"package.json": `
Expand Down Expand Up @@ -755,6 +779,35 @@ describe("publish", () => {
]);
});

test("should use publishFolder", async () => {
mockFs({
"package.json": `
{
"name": "test",
"version": "0.0.0"
}
`,
});
const plugin = new NPMPlugin({ publishFolder: "dist/publish-folder" });
const hooks = makeHooks();

plugin.apply({
config: { prereleaseBranches: ["next"] },
hooks,
remote: "origin",
baseBranch: "main",
logger: dummyLog(),
} as Auto.Auto);

execPromise.mockReturnValueOnce("1.0.0");

await hooks.publish.promise({ bump: Auto.SEMVER.patch });
expect(execPromise).toHaveBeenCalledWith("npm", [
"publish",
"dist/publish-folder",
]);
});

test("should bump published version", async () => {
mockFs({
"package.json": `
Expand Down Expand Up @@ -1024,6 +1077,95 @@ describe("canary", () => {
]);
});

test("should use publishFolder", async () => {
mockFs({
"package.json": `
{
"name": "test"
}
`,
});
const plugin = new NPMPlugin({ publishFolder: "dist/publish-folder" });
const hooks = makeHooks();

plugin.apply(({
config: { prereleaseBranches: ["next"] },
hooks,
remote: "origin",
baseBranch: "main",
logger: dummyLog(),
getCurrentVersion: () => "1.2.3",
git: {
getLatestRelease: () => "1.2.3",
getLatestTagInBranch: () => Promise.resolve("1.2.3"),
},
} as unknown) as Auto.Auto);

await hooks.canary.promise({
bump: Auto.SEMVER.patch,
canaryIdentifier: "canary.123.1",
});
expect(execPromise).toHaveBeenCalledWith("npm", [
"publish",
"dist/publish-folder",
"--tag",
"canary",
]);
});

test("monorepo - should use publishFolder", async () => {
const plugin = new NPMPlugin({ publishFolder: "dist/publish-folder" });
const hooks = makeHooks();

plugin.apply({
config: { prereleaseBranches: ["next"] },
hooks,
remote: "origin",
baseBranch: "main",
logger: dummyLog(),
git: {
getLatestRelease: () => Promise.resolve("1.2.3"),
getLatestTagInBranch: () => Promise.resolve("1.2.3"),
},
} as any);

const packages = [
{
path: "path/to/package",
name: "@foobar/app",
version: "1.2.3-canary.0+abcd",
},
{
path: "path/to/package",
name: "@foobar/lib",
version: "1.2.3-canary.0+abcd",
},
];

monorepoPackages.mockReturnValueOnce(packages.map((p) => ({ package: p })));
getLernaPackages.mockImplementation(async () => Promise.resolve(packages));

await hooks.canary.promise({
bump: Auto.SEMVER.patch,
canaryIdentifier: "",
});
expect(execPromise).toHaveBeenCalledWith("npx", [
"lerna",
"publish",
"1.2.4-0",
"--dist-tag",
"canary",
"--contents",
"dist/publish-folder",
"--force-publish",
"--yes",
"--no-git-reset",
"--no-git-tag-version",
"--exact",
"--no-verify-access"
]);
});

test("use handles repos with no tags", async () => {
mockFs({
"package.json": `
Expand Down
Loading