Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/private-deps-config-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@changesets/config": patch
---

Allow private packages to depend on skipped packages without requiring them to also be skipped. `devDependencies` on skipped packages no longer trigger a validation error in config parsing (aligning with the existing CLI behavior). The config validation for skipped-package dependents now also covers packages skipped via `privatePackages.version: false`, not just those in the `ignore` list.
5 changes: 5 additions & 0 deletions .changeset/private-deps-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@changesets/cli": patch
---

Allow private packages to depend on skipped packages without requiring them to also be skipped. Private packages are not published to npm, so it is safe for them to have dependencies on ignored or unversioned packages.
5 changes: 5 additions & 0 deletions .changeset/private-packages-shape-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@changesets/config": patch
---

Added shape validation for the `privatePackages` config option.
14 changes: 14 additions & 0 deletions docs/versioning-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,17 @@ To enable a project to be tracked by changesets, it needs a minimal package.json
"version": "0.0.1"
}
```

## Private dependencies

When a versioned private package (app) depends on another private package that is skipped (either via the `ignore` option or `privatePackages.version: false`), changesets will not require the app to also be skipped. Since private packages are not published to npm, it is safe for them to depend on skipped packages.

For example, if you have an app `A` that depends on a private library `B`, you can ignore `B` while still versioning `A`:

```json
{
"ignore": ["B"]
}
```

This works because `A` is private and will never be published to npm with a stale reference to `B`.
4 changes: 4 additions & 0 deletions packages/assemble-release-plan/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ function assembleReleasePlan(
refinedConfig
);

// Unlike the config/CLI validation graphs, this graph intentionally includes
// devDependencies. While devDeps don't cause version bumps (determineDependents
// assigns type "none"), they must appear in the release plan so that
// apply-release-plan can update their version ranges in package.json.
let dependencyGraph = getDependentsGraph(packages, {
bumpVersionsWithWorkspaceProtocolOnly:
refinedConfig.bumpVersionsWithWorkspaceProtocolOnly,
Expand Down
54 changes: 54 additions & 0 deletions packages/cli/src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,60 @@ describe("cli", () => {
expect(loggerErrorCalls.length).toEqual(0);
});

it("should not throw if a versioned private package depends on an ignored package", async () => {
const cwd = await testdir({
"package.json": JSON.stringify({
private: true,
workspaces: ["packages/*"],
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
version: "1.0.0",
private: true,
dependencies: {
"pkg-b": "1.0.0",
},
}),
"packages/pkg-b/package.json": JSON.stringify({
name: "pkg-b",
version: "1.0.0",
private: true,
}),
".changeset/config.json": JSON.stringify({}),
});

await run(["version"], { ignore: ["pkg-b"] }, cwd);

const loggerErrorCalls = (error as any).mock.calls;
expect(loggerErrorCalls.length).toEqual(0);
});

it("should not throw if a package only has a devDependency on an ignored package", async () => {
const cwd = await testdir({
"package.json": JSON.stringify({
private: true,
workspaces: ["packages/*"],
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
version: "1.0.0",
devDependencies: {
"pkg-b": "1.0.0",
},
}),
"packages/pkg-b/package.json": JSON.stringify({
name: "pkg-b",
version: "1.0.0",
}),
".changeset/config.json": JSON.stringify({}),
});

await run(["version"], { ignore: ["pkg-b"] }, cwd);

const loggerErrorCalls = (error as any).mock.calls;
expect(loggerErrorCalls.length).toEqual(0);
});

it("should throw if `--ignore` flag is used while ignore array is also defined in the config file ", async () => {
const cwd = await testdir({
"package.json": JSON.stringify({
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ export async function run(
packages.packages.map((x) => [x.packageJson.name, x])
);

// validate that all dependents of skipped packages are also skipped
// Validate that all dependents of skipped packages are also skipped.
// devDependencies are excluded because they don't affect published consumers —
// a stale devDep range on a skipped package is harmless.
// Note: assemble-release-plan uses a graph WITH devDeps because it needs to
// update devDep ranges in package.json even though they don't cause version bumps.
const dependentsGraph = getDependentsGraph(packages, {
ignoreDevDependencies: true,
bumpVersionsWithWorkspaceProtocolOnly:
Expand All @@ -164,6 +168,13 @@ export async function run(
const dependents = dependentsGraph.get(skippedPackage) || [];
for (const dependent of dependents) {
const dependentPkg = packagesByName.get(dependent)!;
if (dependentPkg.packageJson.private) {
// Private packages don't publish to npm,
// so they can safely depend on skipped packages.
// This also holds for private packages with other publish targets (like a VS Code extension)
// as those typically have to prebundle dependencies.
continue;
}
if (
!shouldSkipPackage(dependentPkg, {
ignore: config.ignore,
Expand Down
1 change: 1 addition & 0 deletions packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@changesets/errors": "^0.2.0",
"@changesets/get-dependents-graph": "^2.1.3",
"@changesets/logger": "^0.1.1",
"@changesets/should-skip-package": "^0.1.2",
"@changesets/types": "^6.1.0",
"@manypkg/get-packages": "^1.1.3",
"fs-extra": "^7.0.1",
Expand Down
139 changes: 138 additions & 1 deletion packages/config/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,144 @@ describe("parser errors", () => {
)
).toThrowErrorMatchingInlineSnapshot(`
"Some errors occurred when validating the changesets config:
The package "pkg-a" depends on the ignored package "pkg-b", but "pkg-a" is not being ignored. Please add "pkg-a" to the \`ignore\` option."
The package "pkg-a" depends on the skipped package "pkg-b", but "pkg-a" is not being skipped. Please add "pkg-a" to the \`ignore\` option."
`);
});

test("ignore should not require private versioned dependents to also be ignored", () => {
expect(() =>
unsafeParse(
{ ignore: ["pkg-b"] },
{
...defaultPackages,
packages: [
{
packageJson: {
name: "pkg-a",
private: true,
version: "1.0.0",
dependencies: { "pkg-b": "1.0.0" },
},
dir: "dir",
},
{
packageJson: { name: "pkg-b", version: "1.0.0" },
dir: "dir",
},
],
}
)
).not.toThrow();
});

test("ignore should still require non-private dependents to be ignored even with privatePackages.version enabled", () => {
expect(() =>
unsafeParse(
{ ignore: ["pkg-b"] },
{
...defaultPackages,
packages: [
{
packageJson: {
name: "pkg-a",
version: "1.0.0",
dependencies: { "pkg-b": "1.0.0" },
},
dir: "dir",
},
{
packageJson: { name: "pkg-b", version: "1.0.0" },
dir: "dir",
},
],
}
)
).toThrowErrorMatchingInlineSnapshot(`
"Some errors occurred when validating the changesets config:
The package "pkg-a" depends on the skipped package "pkg-b", but "pkg-a" is not being skipped. Please add "pkg-a" to the \`ignore\` option."
`);
});

test("ignore should not require dev dependents of ignored packages to also be ignored", () => {
expect(() =>
unsafeParse(
{ ignore: ["pkg-b"] },
{
...defaultPackages,
packages: [
{
packageJson: {
name: "pkg-a",
version: "1.0.0",
devDependencies: { "pkg-b": "1.0.0" },
},
dir: "dir",
},
{
packageJson: { name: "pkg-b", version: "1.0.0" },
dir: "dir",
},
],
}
)
).not.toThrow();
});

test("ignore should not require private dependents to be ignored even when privatePackages versioning is disabled", () => {
expect(() =>
unsafeParse(
{ ignore: ["pkg-b"], privatePackages: false },
{
...defaultPackages,
packages: [
{
packageJson: {
name: "pkg-a",
private: true,
version: "1.0.0",
dependencies: { "pkg-b": "1.0.0" },
},
dir: "dir",
},
{
packageJson: { name: "pkg-b", version: "1.0.0" },
dir: "dir",
},
],
}
)
).not.toThrow();
});

test("should error when a public package depends on a private package skipped via privatePackages.version: false", () => {
expect(() =>
unsafeParse(
{ privatePackages: { version: false, tag: false } },
{
...defaultPackages,
packages: [
{
packageJson: {
name: "pkg-a",
version: "1.0.0",
dependencies: { "pkg-b": "1.0.0" },
},
dir: "dir",
},
{
packageJson: {
name: "pkg-b",
private: true,
version: "1.0.0",
},
dir: "dir",
},
],
}
)
).toThrowErrorMatchingInlineSnapshot(`
"Some errors occurred when validating the changesets config:
The package "pkg-a" depends on the skipped package "pkg-b", but "pkg-a" is not being skipped. Please add "pkg-a" to the \`ignore\` option."
`);
});

Expand Down
Loading