Skip to content

Commit

Permalink
Merge pull request #2115 from kclarkey/feat/publishfolder-support
Browse files Browse the repository at this point in the history
feat(plugins/npm): add support for passing publishFolder.
  • Loading branch information
hipstersmoothie authored Mar 4, 2022
2 parents 837aa6a + 10be1b9 commit d715128
Show file tree
Hide file tree
Showing 4 changed files with 347 additions and 3 deletions.
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

0 comments on commit d715128

Please sign in to comment.