Skip to content

Commit

Permalink
feat(publish): workspace:* (or ~) protocol should strictly match range
Browse files Browse the repository at this point in the history
- when releasing the `workspace:` protocol, it was found afterward that it wasn't stricly following the pnpm/yarn workspace protocol where `workspace:*` should be exact range "1.5.0" and `workspace:~` a patch range "~1.5.0", this PR adds this and enables it as the new default (most users will want that) but can be disabled if required
- this is a bit of Breaking Change for whoever started using the `workspace:` protocol with previous version `1.2.0` that was released earlier
  • Loading branch information
ghiscoding committed May 12, 2022
1 parent 67c6144 commit acede60
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 110 deletions.
4 changes: 4 additions & 0 deletions packages/cli/src/cli-commands/cli-publish-commands.ts
Expand Up @@ -109,6 +109,10 @@ exports.builder = (yargs) => {
hidden: true,
type: 'boolean',
},
'workspace-strict-match': {
describe: 'Strict match transform version numbers to an exact range (like "1.2.3") rather than with a caret (like ^1.2.3) when using `workspace:*`.',
type: 'boolean',
},
// y: {
// describe: 'Skip all confirmation prompts.',
// alias: 'yes',
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/__tests__/package.spec.ts
Expand Up @@ -422,7 +422,7 @@ describe('Package.lazy()', () => {
});

it('returns package instance from json and dir arguments', () => {
const pkg = Package.lazy({ name: 'bar', version: '1.2.3' }, '/foo/bar');
const pkg = Package.lazy({ name: 'bar', version: '1.2.3' } as RawManifest, '/foo/bar');

expect(pkg).toBeInstanceOf(Package);
expect(pkg.version).toBe('1.2.3');
Expand Down
72 changes: 43 additions & 29 deletions packages/core/src/package.ts
Expand Up @@ -244,9 +244,10 @@ export class Package {
* @param {Object} resolved npa metadata
* @param {String} depVersion semver
* @param {String} savePrefix npm_config_save_prefix
* @param {Boolean} workspaceStrictMatch - are we using strict match with `workspace:` protocol
* @param {String} updatedByCommand - which command called this update?
*/
updateLocalDependency(resolved: NpaResolveResult, depVersion: string, savePrefix: string, updatedByCommand?: CommandType) {
updateLocalDependency(resolved: NpaResolveResult, depVersion: string, savePrefix: string, workspaceStrictMatch = true, updatedByCommand?: CommandType) {
const depName = resolved.name as string;

// first, try runtime dependencies
Expand All @@ -262,38 +263,51 @@ export class Package {
depCollection = this.devDependencies;
}

if (depCollection) {

if (resolved.registry || resolved.type === 'directory') {
// a version (1.2.3) OR range (^1.2.3) OR directory (file:../foo-pkg)
depCollection[depName] = `${savePrefix}${depVersion}`;

// when using explicit workspace protocol and we're not doing a Publish
// if we are publishing, we will skip this and so we'll keep regular semver range, e.g.: "workspace:*"" will be converted to "^1.2.3"
if (resolved.explicitWorkspace && updatedByCommand !== 'publish') {
if (depCollection && (resolved.registry || resolved.type === 'directory')) {
// a version (1.2.3) OR range (^1.2.3) OR directory (file:../foo-pkg)
depCollection[depName] = `${savePrefix}${depVersion}`;

// when using explicit `workspace:` protocol
if (resolved.explicitWorkspace) {
const workspaceTarget = resolved?.workspaceTarget ?? '';

if (updatedByCommand === 'publish') {
// when publishing, workspace protocol will be transformed to semver range
// e.g.: considering version is `1.2.3` and we have `workspace:*` it will be converted to "^1.2.3" or to "1.2.3" with strict match range enabled
if (workspaceStrictMatch) {
if (workspaceTarget === 'workspace:*') {
depCollection[depName] = depVersion; // exact range
} else if (workspaceTarget === 'workspace:~') {
depCollection[depName] = `~${depVersion}`; // patch range (~)
}
// anything else will fall under minor range (^)
}
} else {
// when versioning we'll only bump workspace protocol that have semver range like `workspace:^1.2.3`
// any other workspace will remain the same in `package.json` file, for example `workspace:^`
// keep target workspace or bump when it's a workspace semver range (like `workspace:^1.2.3`)
depCollection[depName] = /^workspace:[*|^|~]{1}$/.test(resolved?.workspaceTarget ?? '')
? resolved.workspaceTarget // target like `workspace:*`
depCollection[depName] = /^workspace:[*|^|~]{1}$/.test(workspaceTarget)
? resolved.workspaceTarget // target like `workspace:^`
: `workspace:${depCollection[depName]}`; // range like `workspace:^1.2.3`
}
} else if (resolved.gitCommittish) {
// a git url with matching committish (#v1.2.3 or #1.2.3)
const [tagPrefix] = /^\D*/.exec(resolved.gitCommittish) as RegExpExecArray;

// update committish
const { hosted } = resolved as any; // take that, lint!
hosted.committish = `${tagPrefix}${depVersion}`;

// always serialize the full url (identical to previous resolved.saveSpec)
depCollection[depName] = hosted.toString({ noGitPlus: false, noCommittish: false });
} else if (resolved.gitRange) {
// a git url with matching gitRange (#semver:^1.2.3)
const { hosted } = resolved as any; // take that, lint!
hosted.committish = `semver:${savePrefix}${depVersion}`;

// always serialize the full url (identical to previous resolved.saveSpec)
depCollection[depName] = hosted.toString({ noGitPlus: false, noCommittish: false });
}
} else if (resolved.gitCommittish) {
// a git url with matching committish (#v1.2.3 or #1.2.3)
const [tagPrefix] = /^\D*/.exec(resolved.gitCommittish) as RegExpExecArray;

// update committish
const { hosted } = resolved as any; // take that, lint!
hosted.committish = `${tagPrefix}${depVersion}`;

// always serialize the full url (identical to previous resolved.saveSpec)
depCollection[depName] = hosted.toString({ noGitPlus: false, noCommittish: false });
} else if (resolved.gitRange) {
// a git url with matching gitRange (#semver:^1.2.3)
const { hosted } = resolved as any; // take that, lint!
hosted.committish = `semver:${savePrefix}${depVersion}`;

// always serialize the full url (identical to previous resolved.saveSpec)
depCollection[depName] = hosted.toString({ noGitPlus: false, noCommittish: false });
}
}
}
26 changes: 21 additions & 5 deletions packages/publish/README.md
Expand Up @@ -70,6 +70,8 @@ This is useful when a previous `lerna publish` failed to publish all packages to
- [semver `--bump from-git`](#semver--bump-from-git)
- [semver `--bump from-package`](#semver--bump-from-package)
- [`workspace:` protocol](#workspace-protocol)
- [`--workspace-strict-match (default)`](#--workspace-strict-match-default)
- [`--no-workspace-strict-match`](#--workspace-strict-match-default)
- [Options](#options)
- [`--canary`](#--canary)
- [`--contents <dir>`](#--contents-dir)
Expand Down Expand Up @@ -418,8 +420,7 @@ lerna will run [npm lifecycle scripts](https://docs.npmjs.com/cli/v8/using-npm/s


## `workspace:` protocol
The `workspace:` protocol (yarn/pnpm), is also supported by Lerna-Lite. When publishing, we will replace any `workspace:` dependency by:

The `workspace:` protocol (pnpm/yarn) is also supported by Lerna-Lite. When publishing, it will replace any `workspace:` dependency by:
- the corresponding version in the target workspace (if you use `workspace:*`, `workspace:~`, or `workspace:^`)
- the associated semver range (for any other range type)

Expand All @@ -435,15 +436,30 @@ So for example, if we have `foo`, `bar`, `qar`, `zoo` in the workspace and they
}
```

Will be transformed and publish the following:
### with `--workspace-strict-match` (default)
#### When using strict match (default), it will be transformed and publish with the following:
_this is the default and is usually what most user will want to use since it will stricly adhere to pnpm/yarn workspace protocol._
```json
{
"dependencies": {
"foo": "^1.5.0",
"foo": "1.5.0",
"bar": "~1.5.0",
"qar": "^1.5.0",
"zoo": "^1.5.0"
}
}
```
**NOTE:** you might have noticed that `foo` is at `^1.5.0` instead of `1.5.0` and that is expected because Lerna automatically adds the caret `^` when not specified, unless the [version --exact](https://github.com/ghiscoding/lerna-lite/tree/main/packages/version#--exact) option is provided.

### with `--no-workspace-strict-match`
#### When strict match is disabled, it will be transformed and publish with the following:
_you would rarely want to disable the strict match, if you do so then in most use case Lerna will use the caret (^) unless the option [--exact](https://github.com/ghiscoding/lerna-lite/tree/main/packages/version#--exact) is provided._
```json
{
"dependencies": {
"foo": "^1.5.0",
"bar": "^1.5.0",
"qar": "^1.5.0",
"zoo": "^1.5.0"
}
}
```
Expand Up @@ -62,79 +62,159 @@ describe("workspace protocol 'workspace:' specifiers", () => {
await gitCommit(cwd, "setup");
};

it("overwrites workspace protocol with local minor bump version before npm publish but after git commit", async () => {
const cwd = await initFixture("workspace-protocol-specs");

await gitTag(cwd, "v1.0.0");
await setupChanges(cwd);
await new PublishCommand(createArgv(cwd, "--bump", "minor", "--yes"));

expect(writePkg.updatedVersions()).toEqual({
"package-1": "1.1.0",
"package-2": "1.1.0",
"package-3": "1.1.0",
"package-4": "1.1.0",
"package-5": "1.1.0",
"package-6": "1.1.0",
"package-7": "1.1.0",
describe('workspace-strict-match disabled', () => {
it("overwrites workspace protocol with local patch bump version before npm publish but after git commit", async () => {
const cwd = await initFixture("workspace-protocol-specs");

await gitTag(cwd, "v1.0.0");
await setupChanges(cwd);
await new PublishCommand(createArgv(cwd, "--bump", "patch", "--yes", "--no-workspace-strict-match"));

expect(writePkg.updatedVersions()).toEqual({
"package-1": "1.0.1",
"package-2": "1.0.1",
"package-3": "1.0.1",
"package-4": "1.0.1",
"package-5": "1.0.1",
"package-6": "1.0.1",
"package-7": "1.0.1",
});

// notably missing is package-1, which has no relative file: dependencies
expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
"package-1": "^1.0.1", // workspace:*
});
expect(writePkg.updatedManifest("package-3").dependencies).toMatchObject({
"package-2": "^1.0.1", // workspace:^
});
expect(writePkg.updatedManifest("package-4").optionalDependencies).toMatchObject({
"package-3": "^1.0.1", // workspace:~
});
expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
// all fixed versions are bumped when major
"package-4": "^1.0.1", // workspace:^1.0.0
"package-6": "^1.0.1", // workspace:^1.0.0
});
// private packages do not need local version resolution
expect(writePkg.updatedManifest("package-7").dependencies).toMatchObject({
"package-1": "^1.0.1", // ^1.0.0
});
});

// notably missing is package-1, which has no relative file: dependencies
expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
"package-1": "^1.1.0",
});
expect(writePkg.updatedManifest("package-3").dependencies).toMatchObject({
"package-2": "^1.1.0",
});
expect(writePkg.updatedManifest("package-4").optionalDependencies).toMatchObject({
"package-3": "^1.1.0",
});
expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
"package-4": "^1.1.0",
// all fixed versions are bumped when major
"package-6": "^1.1.0",
});
// private packages do not need local version resolution
expect(writePkg.updatedManifest("package-7").dependencies).toMatchObject({
"package-1": "^1.1.0",
it("overwrites workspace protocol with local minor bump version before npm publish but after git commit", async () => {
const cwd = await initFixture("workspace-protocol-specs");

await gitTag(cwd, "v1.0.0");
await setupChanges(cwd);
await new PublishCommand(createArgv(cwd, "--bump", "minor", "--yes", "--no-workspace-strict-match"));

expect(writePkg.updatedVersions()).toEqual({
"package-1": "1.1.0",
"package-2": "1.1.0",
"package-3": "1.1.0",
"package-4": "1.1.0",
"package-5": "1.1.0",
"package-6": "1.1.0",
"package-7": "1.1.0",
});

// notably missing is package-1, which has no relative file: dependencies
expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
"package-1": "^1.1.0", // workspace:*
});
expect(writePkg.updatedManifest("package-3").dependencies).toMatchObject({
"package-2": "^1.1.0", // workspace:^
});
expect(writePkg.updatedManifest("package-4").optionalDependencies).toMatchObject({
"package-3": "^1.1.0", // workspace:~
});
expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
// all fixed versions are bumped when major
"package-4": "^1.1.0", // workspace:^1.0.0
"package-6": "^1.1.0", // workspace:^1.0.0
});
// private packages do not need local version resolution
expect(writePkg.updatedManifest("package-7").dependencies).toMatchObject({
"package-1": "^1.1.0", // ^1.0.0
});
});
});

it("overwrites workspace protocol with local major bump version before npm publish but after git commit", async () => {
const cwd = await initFixture("workspace-protocol-specs");

await gitTag(cwd, "v1.0.0");
await setupChanges(cwd);
await new PublishCommand(createArgv(cwd, "--bump", "major", "--yes"));

expect(writePkg.updatedVersions()).toEqual({
"package-1": "2.0.0",
"package-2": "2.0.0",
"package-3": "2.0.0",
"package-4": "2.0.0",
"package-5": "2.0.0",
"package-6": "2.0.0",
"package-7": "2.0.0",
describe('workspace-strict-match enabled', () => {
it("overwrites workspace protocol with local minor bump version before npm publish but after git commit", async () => {
const cwd = await initFixture("workspace-protocol-specs");

await gitTag(cwd, "v1.0.0");
await setupChanges(cwd);
await new PublishCommand(createArgv(cwd, "--bump", "minor", "--yes", "--workspace-strict-match"));

expect(writePkg.updatedVersions()).toEqual({
"package-1": "1.1.0",
"package-2": "1.1.0",
"package-3": "1.1.0",
"package-4": "1.1.0",
"package-5": "1.1.0",
"package-6": "1.1.0",
"package-7": "1.1.0",
});

// notably missing is package-1, which has no relative file: dependencies
expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
"package-1": "1.1.0", // workspace:*
});
expect(writePkg.updatedManifest("package-3").dependencies).toMatchObject({
"package-2": "^1.1.0", // workspace:^
});
expect(writePkg.updatedManifest("package-4").optionalDependencies).toMatchObject({
"package-3": "~1.1.0", // workspace:~
});
expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
// all fixed versions are bumped when major
"package-4": "^1.1.0", // workspace:^1.0.0
"package-6": "^1.1.0", // workspace:^1.0.0
});
// private packages do not need local version resolution
expect(writePkg.updatedManifest("package-7").dependencies).toMatchObject({
"package-1": "^1.1.0", // ^1.0.0
});
});

// notably missing is package-1, which has no relative file: dependencies
expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
"package-1": "^2.0.0",
});
expect(writePkg.updatedManifest("package-3").dependencies).toMatchObject({
"package-2": "^2.0.0",
});
expect(writePkg.updatedManifest("package-4").optionalDependencies).toMatchObject({
"package-3": "^2.0.0",
});
expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
"package-4": "^2.0.0",
// all fixed versions are bumped when major
"package-6": "^2.0.0",
});
// private packages do not need local version resolution
expect(writePkg.updatedManifest("package-7").dependencies).toMatchObject({
"package-1": "^2.0.0",
it("overwrites workspace protocol with local major bump version before npm publish but after git commit", async () => {
const cwd = await initFixture("workspace-protocol-specs");

await gitTag(cwd, "v1.0.0");
await setupChanges(cwd);
await new PublishCommand(createArgv(cwd, "--bump", "major", "--yes", "--workspace-strict-match"));

expect(writePkg.updatedVersions()).toEqual({
"package-1": "2.0.0",
"package-2": "2.0.0",
"package-3": "2.0.0",
"package-4": "2.0.0",
"package-5": "2.0.0",
"package-6": "2.0.0",
"package-7": "2.0.0",
});

// notably missing is package-1, which has no relative file: dependencies
expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
"package-1": "2.0.0", // workspace:*
});
expect(writePkg.updatedManifest("package-3").dependencies).toMatchObject({
"package-2": "^2.0.0", // workspace:^
});
expect(writePkg.updatedManifest("package-4").optionalDependencies).toMatchObject({
"package-3": "~2.0.0", // workspace:~
});
expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
// all fixed versions are bumped when major
"package-4": "^2.0.0", // workspace:^1.0.0
"package-6": "^2.0.0", // workspace:^1.0.0
});
// private packages do not need local version resolution
expect(writePkg.updatedManifest("package-7").dependencies).toMatchObject({
"package-1": "^2.0.0", // ^1.0.0
});
});
});
});

0 comments on commit acede60

Please sign in to comment.