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: first iteration of monorepo support #15

Merged
merged 3 commits into from
Dec 7, 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
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ Use this plugin instead of the default
[@semantic-release/npm](https://github.com/semantic-release/npm) if you want to
use Yarn instead of the NPM CLI to publish your packages to the NPM registry.

As an added bonus, this plugin will also publish some simple monorepo patterns
(currently WIP).
As an added bonus, this plugin will also publish some simple monorepo patterns.

## Table of contents

Expand All @@ -19,6 +18,7 @@ As an added bonus, this plugin will also publish some simple monorepo patterns
- [Install](#install)
- [Usage](#usage)
- [NPM registry authentication](#npm-registry-authentication)
- [Monorepo support](#monorepo-support)
- [Configuration](#configuration)
- [Environment variables](#environment-variables)
- [`.yarnrc.yml` file](#yarnrcyml-file)
Expand Down Expand Up @@ -77,6 +77,24 @@ The NPM authentication configuration is **required** and can be set either via
> (`username:password`) authentication is strongly discouraged and not supported
> by this plugin.

## Monorepo support

Currently, simple monorepo versioning and publishing is supported. All
workspaces versions will be aligned (a.k.a. fixed/locked mode) and when a new
release is due, all workspaces will be published to the NPM registry.

Monorepos are detected by the presence of a
[`workspaces`](https://yarnpkg.com/configuration/manifest#workspaces) option in
the root `package.json` file:

```json
{
"workspaces": ["packages/*"]
}
```

See [our roadmap](#roadmap) for further implementation status.

## Configuration

### Environment variables
Expand Down Expand Up @@ -195,6 +213,12 @@ For example with the
## Roadmap

- [ ] Monorepo support
- [x] Support for fixed versions
- [x] Support for private/non-private root package
- [ ] Support for channels (added failing tests)
- [ ] Support for release information for each workspace
- [ ] Support for independant versions (probably impossible without custom
analyze-commits plugin)
- [ ] Get rid of CJS build once
[upstream PR 2607](https://github.com/semantic-release/semantic-release/pull/2607)
lands
Expand Down
24 changes: 16 additions & 8 deletions src/add-channel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolve } from "node:path";
import type { PackageJson } from "read-pkg";
import { getImplementation } from "./container.js";
import type { AddChannelContext } from "./definitions/context.js";
Expand All @@ -6,9 +7,10 @@ import { getChannel } from "./get-channel.js";
import { getRegistry } from "./get-registry.js";
import { getReleaseInfo } from "./get-release-info.js";
import { getYarnConfig } from "./get-yarn-config.js";
import { reasonToNotPublish, shouldPublish } from "./should-publish.js";

export async function addChannel(
{ npmPublish }: PluginConfig,
pluginConfig: PluginConfig,
pkg: PackageJson,
context: AddChannelContext
) {
Expand All @@ -20,12 +22,20 @@ export async function addChannel(
nextRelease: { version, channel },
logger,
} = context;
const { pkgRoot } = pluginConfig;
const execa = await getImplementation("execa");

if (npmPublish !== false && pkg.private !== true) {
if (shouldPublish(pluginConfig, pkg)) {
const basePath = pkgRoot ? resolve(cwd, String(pkgRoot)) : cwd;
const yarnrc = await getYarnConfig(context);
const registry = getRegistry(pkg, yarnrc, context);
const distTag = getChannel(channel!);
const isMonorepo = typeof pkg.workspaces !== "undefined";

if (isMonorepo) {
logger.log(`Adding npm tags to monorepo workspaces is not supported yet`);
return false;
}

logger.log(
`Adding version ${version} to npm registry ${registry} on dist-tag ${distTag}`
Expand All @@ -34,7 +44,7 @@ export async function addChannel(
"yarn",
["npm", "tag", "add", `${pkg.name}@${version}`, distTag],
{
cwd,
cwd: basePath,
env,
}
);
Expand All @@ -49,11 +59,9 @@ export async function addChannel(
return getReleaseInfo(pkg, context, distTag, registry);
}

logger.log(
`Skip adding to npm channel as ${
npmPublish === false ? "npmPublish" : "package.json's private property"
} is ${npmPublish !== false}`
);
const reason = reasonToNotPublish(pluginConfig, pkg);

logger.log(`Skip adding to npm channel as ${reason}`);

return false;
}
10 changes: 6 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export async function prepare(
await verify(pluginConfig, context);
}

await prepareNpm(pluginConfig, context);
const pkg = await getPkg(pluginConfig, context);

await prepareNpm(pluginConfig, pkg, context);

prepared = true;
}
Expand All @@ -71,12 +73,12 @@ export async function publish(
await verify(pluginConfig, context);
}

const pkg = await getPkg(pluginConfig, context);

if (!prepared) {
await prepareNpm(pluginConfig, context);
await prepareNpm(pluginConfig, pkg, context);
}

const pkg = await getPkg(pluginConfig, context);

return publishNpm(pluginConfig, pkg, context);
}

Expand Down
49 changes: 44 additions & 5 deletions src/prepare.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "fs-extra";
import { resolve } from "node:path";
import type { PackageJson } from "read-pkg";
import { getImplementation } from "./container.js";
import type { PrepareContext } from "./definitions/context.js";
import type { PluginConfig } from "./definitions/pluginConfig.js";
Expand All @@ -8,10 +9,39 @@ const TARBALL_FILENAME = "%s-%v.tgz";

export async function prepare(
{ tarballDir, pkgRoot }: PluginConfig,
pkg: PackageJson,
{ cwd, env, stdout, stderr, nextRelease: { version }, logger }: PrepareContext
) {
const basePath = pkgRoot ? resolve(cwd, String(pkgRoot)) : cwd;
const execa = await getImplementation("execa");
const isMonorepo = typeof pkg.workspaces !== "undefined";
const workspacesPrefix = isMonorepo
? ["workspaces", "foreach", "--topological", "--verbose", "--no-private"]
: [];

if (isMonorepo) {
logger.log("Installing Yarn workspace-tools plugin in %s", basePath);
const pluginImportResult = execa(
"yarn",
["plugin", "import", "workspace-tools"],
{
cwd: basePath,
env,
}
);
pluginImportResult.stdout!.pipe(stdout, { end: false });
pluginImportResult.stderr!.pipe(stderr, { end: false });
await pluginImportResult;

logger.log("Running `yarn install` in %s", basePath);
const yarnInstallResult = execa("yarn", ["install", "--no-immutable"], {
cwd: basePath,
env,
});
yarnInstallResult.stdout!.pipe(stdout, { end: false });
yarnInstallResult.stderr!.pipe(stderr, { end: false });
await yarnInstallResult;
}

logger.log("Installing Yarn version plugin in %s", basePath);
const pluginImportResult = execa("yarn", ["plugin", "import", "version"], {
Expand All @@ -23,10 +53,14 @@ export async function prepare(
await pluginImportResult;

logger.log("Write version %s to package.json in %s", version, basePath);
const versionResult = execa("yarn", ["version", version, "--immediate"], {
cwd: basePath,
env,
});
const versionResult = execa(
"yarn",
[...workspacesPrefix, "version", version, "--immediate"],
{
cwd: basePath,
env,
}
);
versionResult.stdout!.pipe(stdout, { end: false });
versionResult.stderr!.pipe(stderr, { end: false });
await versionResult;
Expand All @@ -36,7 +70,12 @@ export async function prepare(
await fs.ensureDir(resolve(cwd, tarballDir.trim()));
const packResult = execa(
"yarn",
["pack", "--out", resolve(cwd, tarballDir.trim(), TARBALL_FILENAME)],
[
...workspacesPrefix,
"pack",
"--out",
resolve(cwd, tarballDir.trim(), TARBALL_FILENAME),
],
{
cwd: basePath,
env,
Expand Down
54 changes: 43 additions & 11 deletions src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { getChannel } from "./get-channel.js";
import { getRegistry } from "./get-registry.js";
import { getReleaseInfo } from "./get-release-info.js";
import { getYarnConfig } from "./get-yarn-config.js";
import { reasonToNotPublish, shouldPublish } from "./should-publish.js";

export async function publish(
{ npmPublish, pkgRoot }: PluginConfig,
pluginConfig: PluginConfig,
pkg: PackageJson,
context: PublishContext
) {
Expand All @@ -21,21 +22,54 @@ export async function publish(
nextRelease: { version, channel },
logger,
} = context;
const { pkgRoot } = pluginConfig;
const execa = await getImplementation("execa");

if (npmPublish !== false && pkg.private !== true) {
if (shouldPublish(pluginConfig, pkg)) {
const basePath = pkgRoot ? resolve(cwd, String(pkgRoot)) : cwd;
const yarnrc = await getYarnConfig(context);
const registry = getRegistry(pkg, yarnrc, context);
const distTag = getChannel(channel!);
const isMonorepo = typeof pkg.workspaces !== "undefined";
const workspacesPrefix = isMonorepo
? ["workspaces", "foreach", "--topological", "--verbose", "--no-private"]
: [];

if (isMonorepo) {
logger.log("Installing Yarn workspace-tools plugin in %s", basePath);
const pluginImportResult = execa(
"yarn",
["plugin", "import", "workspace-tools"],
{
cwd: basePath,
env,
}
);
pluginImportResult.stdout!.pipe(stdout, { end: false });
pluginImportResult.stderr!.pipe(stderr, { end: false });
await pluginImportResult;

logger.log("Running `yarn install` in %s", basePath);
const yarnInstallResult = execa("yarn", ["install", "--no-immutable"], {
cwd: basePath,
env,
});
yarnInstallResult.stdout!.pipe(stdout, { end: false });
yarnInstallResult.stderr!.pipe(stderr, { end: false });
await yarnInstallResult;
}

logger.log(
`Publishing version ${version} to npm registry ${registry} on dist-tag ${distTag}`
);
const result = execa("yarn", ["npm", "publish", "--tag", distTag], {
cwd: basePath,
env,
});
const result = execa(
"yarn",
[...workspacesPrefix, "npm", "publish", "--tag", distTag],
{
cwd: basePath,
env,
}
);
result.stdout!.pipe(stdout, { end: false });
result.stderr!.pipe(stderr, { end: false });
await result;
Expand All @@ -47,11 +81,9 @@ export async function publish(
return getReleaseInfo(pkg, context, distTag, registry);
}

logger.log(
`Skip publishing to npm registry as ${
npmPublish === false ? "npmPublish" : "package.json's private property"
} is ${npmPublish !== false}`
);
const reason = reasonToNotPublish(pluginConfig, pkg);

logger.log(`Skip publishing to npm registry as ${reason}`);

return false;
}
34 changes: 34 additions & 0 deletions src/should-publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { PackageJson } from "read-pkg";
import type { PluginConfig } from "./definitions/pluginConfig.js";

/**
* Returns [true, null] if `npmPublish` is not `false` and `pkg.private` is not
* `true` or `pkg.workspaces` is not `undefined`.
* Returns [false, reason] otherwise.
*/
function shouldPublishTuple(
pluginConfig: PluginConfig,
pkg: PackageJson
): [boolean, string | null] {
const reasonToNotPublish =
pluginConfig.npmPublish === false
? "npmPublish plugin option is false"
: pkg.private === true && typeof pkg.workspaces === "undefined"
? "package is private and has no workspaces"
: null;

const shouldPublish = !reasonToNotPublish;

return [shouldPublish, reasonToNotPublish];
}

export function shouldPublish(pluginConfig: PluginConfig, pkg: PackageJson) {
return shouldPublishTuple(pluginConfig, pkg)[0];
}

export function reasonToNotPublish(
pluginConfig: PluginConfig,
pkg: PackageJson
) {
return shouldPublishTuple(pluginConfig, pkg)[1];
}
5 changes: 2 additions & 3 deletions src/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getImplementation } from "./container.js";
import type { VerifyConditionsContext } from "./definitions/context.js";
import type { PluginConfig } from "./definitions/pluginConfig.js";
import { getPkg } from "./get-pkg.js";
import { shouldPublish } from "./should-publish.js";
import { verifyAuth } from "./verify-auth.js";
import { verifyConfig } from "./verify-config.js";
import { verifyYarn } from "./verify-yarn.js";
Expand All @@ -23,9 +24,7 @@ export async function verify(
try {
const pkg = await getPkg(pluginConfig, context);

// Verify the npm authentication only if `npmPublish` is not false and
// `pkg.private` is not`true`
if (pluginConfig.npmPublish !== false && pkg.private !== true) {
if (shouldPublish(pluginConfig, pkg)) {
await verifyAuth(pluginConfig, pkg, context);
}
} catch (error: any) {
Expand Down
Loading