From ee57dfbb3ad26cd4bd722e1b54941360ec22f698 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Sun, 8 May 2022 02:08:50 -0400 Subject: [PATCH] feat(core): add version/publish `workspace:` protocol - add few unit tests under Package/PackageGraph & Publish --- packages/core/src/__tests__/package.spec.ts | 74 ++++++++- packages/core/src/command.ts | 16 +- packages/core/src/models/index.ts | 3 + .../__tests__/package-graph.spec.ts | 39 ++++- .../core/src/package-graph/package-graph.ts | 15 ++ packages/core/src/package.ts | 15 +- packages/exec/src/exec-command.ts | 3 +- packages/info/src/info-command.ts | 4 +- .../workspace-protocol-specs/.gitignore | 1 + .../workspace-protocol-specs/.npmrc | 1 + .../workspace-protocol-specs/lerna.json | 3 + .../workspace-protocol-specs/package.json | 10 ++ .../packages/package-1/package.json | 8 + .../packages/package-2/package.json | 7 + .../packages/package-3/package.json | 7 + .../packages/package-4/package.json | 7 + .../packages/package-5/package.json | 8 + .../packages/package-6/package.json | 7 + .../packages/package-7/package.json | 8 + ...lish-workspace-protocol-specifiers.test.js | 140 ++++++++++++++++++ packages/publish/src/publish-command.ts | 54 +++++-- packages/run/src/run-command.ts | 3 +- packages/version/src/version-command.ts | 5 +- 23 files changed, 397 insertions(+), 41 deletions(-) create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/.gitignore create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/.npmrc create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/lerna.json create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/package.json create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-1/package.json create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-2/package.json create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-3/package.json create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-4/package.json create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-5/package.json create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-6/package.json create mode 100644 packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-7/package.json create mode 100644 packages/publish/src/__tests__/publish-workspace-protocol-specifiers.test.js diff --git a/packages/core/src/__tests__/package.spec.ts b/packages/core/src/__tests__/package.spec.ts index 846f7e45..72bfdb07 100644 --- a/packages/core/src/__tests__/package.spec.ts +++ b/packages/core/src/__tests__/package.spec.ts @@ -301,7 +301,7 @@ describe('Package', () => { }); describe(".updateLocalDependency()", () => { - it("works with workspace: protocols", () => { + it("works with `workspace:` protocol range", () => { const pkg = factory({ dependencies: { a: "workspace:^1.0.0", @@ -329,6 +329,78 @@ describe('Package', () => { } `); }); + + it("works with star workspace target `workspace:*` and will keep same output target", () => { + const pkg = factory({ + dependencies: { + a: "workspace:*", + b: "workspace:^1.0.0", + }, + }); + + const resolved: NpaResolveResult = npa.resolve("a", "^1.0.0", "."); + resolved.explicitWorkspace = true; + resolved.workspaceTarget = 'workspace:*'; + + pkg.updateLocalDependency(resolved, "2.0.0", "^"); + + expect(pkg.toJSON()).toMatchInlineSnapshot(` + Object { + "dependencies": Object { + "a": "workspace:*", + "b": "workspace:^1.0.0", + }, + } + `); + }); + + it("works with caret workspace target `workspace:^` and will keep same output target", () => { + const pkg = factory({ + dependencies: { + a: "workspace:^", + b: "workspace:^1.0.0", + }, + }); + + const resolved: NpaResolveResult = npa.resolve("a", "^1.0.0", "."); + resolved.explicitWorkspace = true; + resolved.workspaceTarget = 'workspace:^'; + + pkg.updateLocalDependency(resolved, "2.0.0", "^"); + + expect(pkg.toJSON()).toMatchInlineSnapshot(` + Object { + "dependencies": Object { + "a": "workspace:^", + "b": "workspace:^1.0.0", + }, + } + `); + }); + + it("works with tilde workspace target `workspace:~` and will keep same output target", () => { + const pkg = factory({ + dependencies: { + a: "workspace:~", + b: "workspace:^1.0.0", + }, + }); + + const resolved: NpaResolveResult = npa.resolve("a", "^1.0.0", "."); + resolved.explicitWorkspace = true; + resolved.workspaceTarget = 'workspace:~'; + + pkg.updateLocalDependency(resolved, "2.0.0", "^"); + + expect(pkg.toJSON()).toMatchInlineSnapshot(` + Object { + "dependencies": Object { + "a": "workspace:~", + "b": "workspace:^1.0.0", + }, + } + `); + }); }); }); diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts index 28eec8a1..670bf2d2 100644 --- a/packages/core/src/command.ts +++ b/packages/core/src/command.ts @@ -10,7 +10,7 @@ import { warnIfHanging } from './utils/warn-if-hanging'; import { writeLogFile } from './utils/write-log-file'; import { Project } from './project/project'; import { ValidationError } from './validation-error'; -import { ExecOpts } from './models'; +import { CommandType, ExecOpts } from './models'; import { PackageGraph } from './package-graph/package-graph'; import { logExecCommand } from './child-process'; @@ -25,7 +25,7 @@ export class Command { toposort?: number; execOpts!: ExecOpts; - name = ''; + commandName: CommandType = ''; composed; logger!: Logger; options: any; @@ -41,10 +41,10 @@ export class Command { log.silly('argv', argv); // 'FooCommand' => 'foo' - this.name = this.constructor.name.replace(/Command$/, '').toLowerCase(); + this.commandName = (this.constructor.name.replace(/Command$/, '').toLowerCase()) as CommandType; // composed commands are called from other commands, like publish -> version - this.composed = typeof argv.composed === 'string' && argv.composed !== this.name; + this.composed = typeof argv.composed === 'string' && argv.composed !== this.commandName; if (!this.composed) { // composed commands have already logged the lerna version @@ -175,7 +175,7 @@ export class Command { const commandConfig = this.project.config.command || {}; // The current command always overrides otherCommandConfigs - const overrides = [this.name, ...this.otherCommandConfigs].map((key) => (commandConfig as any)[key]); + const overrides = [this.commandName, ...this.otherCommandConfigs].map((key) => (commandConfig as any)[key]); this.options = defaultOptions( // CLI flags, which if defined overrule subsequent values @@ -213,7 +213,7 @@ export class Command { // create logger that subclasses use Object.defineProperty(this, 'logger', { - value: log.newGroup(this.name), + value: log.newGroup(this.commandName), }); // emit all buffered logs at configured level and higher @@ -302,11 +302,11 @@ export class Command { } initialize(): any | Promise { - throw new ValidationError(this.name, 'initialize() needs to be implemented.'); + throw new ValidationError(this.commandName, 'initialize() needs to be implemented.'); } execute(): any | Promise { - throw new ValidationError(this.name, 'execute() needs to be implemented.'); + throw new ValidationError(this.commandName, 'execute() needs to be implemented.'); } } diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts index 656f2413..9a660bf6 100644 --- a/packages/core/src/models/index.ts +++ b/packages/core/src/models/index.ts @@ -18,6 +18,8 @@ export interface CommandOptions { rollVersion?: boolean; } +export type CommandType = '' | 'exec' | 'info' | 'publish' | 'version' | 'run'; + export interface DescribeRefOptions { /* Defaults to `process.cwd()` */ cwd?: string; @@ -141,6 +143,7 @@ export interface GitClient { export type NpaResolveResult = (npa.FileResult | npa.HostedGitResult | npa.URLResult | npa.AliasResult | npa.RegistryResult) & { explicitWorkspace?: boolean; + workspaceTarget?: string; } /** Passed between concurrent executions */ diff --git a/packages/core/src/package-graph/__tests__/package-graph.spec.ts b/packages/core/src/package-graph/__tests__/package-graph.spec.ts index 7ebdb34c..3ec61463 100644 --- a/packages/core/src/package-graph/__tests__/package-graph.spec.ts +++ b/packages/core/src/package-graph/__tests__/package-graph.spec.ts @@ -120,15 +120,42 @@ describe("PackageGraph", () => { }, "/test/pkg-3" ), + new Package( + { + name: "pkg-4", + version: "1.0.0", + dependencies: { + "pkg-1": "workspace:*", + }, + }, + "/test/pkg-4" + ), + new Package( + { + name: "pkg-5", + version: "1.0.0", + dependencies: { + "pkg-1": "workspace:^", + "pkg-2": "workspace:~", + }, + }, + "/test/pkg-5" + ), ]; const graph = new PackageGraph(pkgs, "allDependencies", "explicit"); - const [pkg1, pkg2, pkg3] = graph.values(); - - expect(pkg1.localDependents.has("pkg-2")).toBe(false); - expect(pkg2.localDependencies.has("pkg-1")).toBe(false); - expect(pkg1.localDependents.has("pkg-3")).toBe(true); - expect(pkg3.localDependencies.has("pkg-1")).toBe(true); + const [pkg1, pkg2, pkg3, pkg4, pkg5] = graph.values(); + + expect(pkg1.localDependents.has('pkg-2')).toBe(false); + expect(pkg2.localDependencies.has('pkg-1')).toBe(false); + expect(pkg1.localDependents.has('pkg-3')).toBe(true); + expect(pkg3.localDependencies.has('pkg-1')).toBe(true); + expect(pkg4.localDependencies.has('pkg-1')).toBe(true); + expect(pkg4.localDependencies.get('pkg-1').workspaceTarget).toBe('workspace:*'); + expect(pkg5.localDependencies.has('pkg-1')).toBe(true); + expect(pkg5.localDependencies.has('pkg-2')).toBe(true); + expect(pkg5.localDependencies.get('pkg-1').workspaceTarget).toBe('workspace:^'); + expect(pkg5.localDependencies.get('pkg-2').workspaceTarget).toBe('workspace:~'); }); }); diff --git a/packages/core/src/package-graph/package-graph.ts b/packages/core/src/package-graph/package-graph.ts index a4cdaf79..6f0c5ce9 100644 --- a/packages/core/src/package-graph/package-graph.ts +++ b/packages/core/src/package-graph/package-graph.ts @@ -70,12 +70,27 @@ export class PackageGraph extends Map { // npa doesn't support the explicit workspace: protocol, supported by // pnpm and Yarn. const explicitWorkspace = /^workspace:/.test(spec); + let workspaceTarget: string | undefined; if (explicitWorkspace) { + workspaceTarget = spec; spec = spec.replace(/^workspace:/, ''); + + // when dependency is defined as target workspace, like `workspace:*`, + // we'll have to pull the version from its parent package version property + // example with `1.5.0`, ws:* => "1.5.0", ws:^ => "^1.5.0", ws:~ => "~1.5.0", ws:^1.5.0 => "^1.5.0" + if (spec === '*' || spec === '^' || spec === '~') { + const depPkg = packages.find(pkg => pkg.name === depName); + const version = depPkg?.version; + const specTarget = spec === '*' ? '' : spec; + spec = depPkg ? `${specTarget}${version}` : ''; + } } const resolved: NpaResolveResult = npa.resolve(depName, spec, currentNode.location); resolved.explicitWorkspace = explicitWorkspace; + if (resolved.explicitWorkspace) { + resolved.workspaceTarget = workspaceTarget; + } if (!depNode) { // it's an external dependency, store the resolution and bail diff --git a/packages/core/src/package.ts b/packages/core/src/package.ts index 119d0b04..26677774 100644 --- a/packages/core/src/package.ts +++ b/packages/core/src/package.ts @@ -3,7 +3,7 @@ import path from 'path'; import loadJsonFile from 'load-json-file'; import writePkg from 'write-pkg'; -import { NpaResolveResult, RawManifest } from './models'; +import { CommandType, NpaResolveResult, RawManifest } from './models'; // symbol used to 'hide' internal state const PKG = Symbol('pkg'); @@ -244,8 +244,9 @@ export class Package { * @param {Object} resolved npa metadata * @param {String} depVersion semver * @param {String} savePrefix npm_config_save_prefix + * @param {String} updatedByCommand - which command called this update? */ - updateLocalDependency(resolved: NpaResolveResult, depVersion: string, savePrefix: string) { + updateLocalDependency(resolved: NpaResolveResult, depVersion: string, savePrefix: string, updatedByCommand?: CommandType) { const depName = resolved.name as string; // first, try runtime dependencies @@ -266,8 +267,14 @@ export class Package { 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}`; - if (resolved.explicitWorkspace) { - depCollection[depName] = `workspace:${depCollection[depName]}`; + + // 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') { + // 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:*` + : `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) diff --git a/packages/exec/src/exec-command.ts b/packages/exec/src/exec-command.ts index 79135c72..384744d4 100644 --- a/packages/exec/src/exec-command.ts +++ b/packages/exec/src/exec-command.ts @@ -2,6 +2,7 @@ import 'dotenv/config'; import pMap from 'p-map'; import { Command, + CommandType, logOutput, Package, runTopologically, @@ -19,7 +20,7 @@ export function factory(argv) { export class ExecCommand extends Command { /** command name */ - name = 'exec'; + name = 'exec' as CommandType; args: string[] = []; bail = false; diff --git a/packages/info/src/info-command.ts b/packages/info/src/info-command.ts index ea75a42a..a66257e7 100644 --- a/packages/info/src/info-command.ts +++ b/packages/info/src/info-command.ts @@ -1,4 +1,4 @@ -import { Command, logOutput, } from '@lerna-lite/core'; +import { Command, CommandType, logOutput, } from '@lerna-lite/core'; import envinfo from 'envinfo'; export function factory(argv) { @@ -7,7 +7,7 @@ export function factory(argv) { export class InfoCommand extends Command { /** command name */ - name = 'info'; + name = 'info' as CommandType; constructor(argv: any) { super(argv); diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/.gitignore b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/.npmrc b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/.npmrc new file mode 100644 index 00000000..84cf58e1 --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/.npmrc @@ -0,0 +1 @@ +registry = https://registry.npmjs.org/ \ No newline at end of file diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/lerna.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/lerna.json new file mode 100644 index 00000000..1587a669 --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/lerna.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/package.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/package.json new file mode 100644 index 00000000..61b8b60c --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/package.json @@ -0,0 +1,10 @@ +{ + "name": "workspace-protocol-specs", + "description": "workspace protocol specifiers should act just like recognized semver ranges", + "private": true, + "version": "0.0.0-lerna", + "dependencies": { + "package-2": "workspace:*", + "package-5": "workspace:*" + } +} \ No newline at end of file diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-1/package.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-1/package.json new file mode 100644 index 00000000..105cc0f9 --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-1/package.json @@ -0,0 +1,8 @@ +{ + "name": "package-1", + "version": "1.0.0", + "changed": false, + "dependencies": { + "tiny-tarball": "^1.0.0" + } +} diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-2/package.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-2/package.json new file mode 100644 index 00000000..800bd2a5 --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-2/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-2", + "version": "1.0.0", + "dependencies": { + "package-1": "workspace:*" + } +} \ No newline at end of file diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-3/package.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-3/package.json new file mode 100644 index 00000000..16caf80e --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-3/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-3", + "version": "1.0.0", + "dependencies": { + "package-2": "workspace:^" + } +} \ No newline at end of file diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-4/package.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-4/package.json new file mode 100644 index 00000000..97db50cb --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-4/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-4", + "version": "1.0.0", + "optionalDependencies": { + "package-3": "workspace:~" + } +} \ No newline at end of file diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-5/package.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-5/package.json new file mode 100644 index 00000000..cc665996 --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-5/package.json @@ -0,0 +1,8 @@ +{ + "name": "package-5", + "version": "1.0.0", + "dependencies": { + "package-4": "workspace:^1.0.0", + "package-6": "workspace:~1.0.0" + } +} \ No newline at end of file diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-6/package.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-6/package.json new file mode 100644 index 00000000..d03755aa --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-6/package.json @@ -0,0 +1,7 @@ +{ + "name": "package-6", + "version": "1.0.0", + "dependencies": { + "tiny-tarball": "^1.0.0" + } +} diff --git a/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-7/package.json b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-7/package.json new file mode 100644 index 00000000..521510f6 --- /dev/null +++ b/packages/publish/src/__tests__/__fixtures__/workspace-protocol-specs/packages/package-7/package.json @@ -0,0 +1,8 @@ +{ + "name": "package-7", + "version": "1.0.0", + "private": true, + "dependencies": { + "package-1": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/publish/src/__tests__/publish-workspace-protocol-specifiers.test.js b/packages/publish/src/__tests__/publish-workspace-protocol-specifiers.test.js new file mode 100644 index 00000000..0ca083ab --- /dev/null +++ b/packages/publish/src/__tests__/publish-workspace-protocol-specifiers.test.js @@ -0,0 +1,140 @@ +"use strict"; + +// FIXME: better mock for version command +jest.mock("../../../version/dist/lib/git-push", () => jest.requireActual("../../../version/src/lib/__mocks__/git-push")); +jest.mock("../../../version/dist/lib/is-anything-committed", () => jest.requireActual("../../../version/src/lib/__mocks__/is-anything-committed")); +jest.mock("../../../version/dist/lib/is-behind-upstream", () => jest.requireActual("../../../version/src/lib/__mocks__/is-behind-upstream")); +jest.mock("../../../version/dist/lib/remote-branch-exists", () => jest.requireActual("../../../version/src/lib/__mocks__/remote-branch-exists")); + +// mocked modules of @lerna-lite/core +jest.mock('@lerna-lite/core', () => ({ + ...jest.requireActual('@lerna-lite/core'), // return the other real methods, below we'll mock only 2 of the methods + collectUpdates: jest.requireActual('../../../core/src/__mocks__/collect-updates').collectUpdates, + throwIfUncommitted: jest.requireActual('../../../core/src/__mocks__/check-working-tree').throwIfUncommitted, + getOneTimePassword: () => Promise.resolve("654321"), + logOutput: jest.requireActual('../../../core/src/__mocks__/output').logOutput, + promptConfirmation: jest.requireActual("../../../core/src/__mocks__/prompt").promptConfirmation, + promptSelectOne: jest.requireActual('../../../core/src/__mocks__/prompt').promptSelectOne, + promptTextInput: jest.requireActual('../../../core/src/__mocks__/prompt').promptTextInput, +})); + +// also point to the local publish command so that all mocks are properly used even by the command-runner +jest.mock('@lerna-lite/publish', () => jest.requireActual('../publish-command')); + +// local modules _must_ be explicitly mocked +jest.mock("../lib/get-packages-without-license", () => jest.requireActual('../lib/__mocks__/get-packages-without-license')); +jest.mock("../lib/verify-npm-package-access", () => jest.requireActual('../lib/__mocks__/verify-npm-package-access')); +jest.mock("../lib/get-npm-username", () => jest.requireActual('../lib/__mocks__/get-npm-username')); +jest.mock("../lib/get-two-factor-auth-required", () => jest.requireActual('../lib/__mocks__/get-two-factor-auth-required')); +jest.mock("../lib/pack-directory", () => jest.requireActual('../lib/__mocks__/pack-directory')); +jest.mock("../lib/npm-publish", () => jest.requireActual('../lib/__mocks__/npm-publish')); + +const fs = require("fs-extra"); +const path = require("path"); + +// mocked modules +const writePkg = require("write-pkg"); + +// helpers +const initFixture = require("@lerna-test/init-fixture")(__dirname); +const { gitAdd } = require("@lerna-test/git-add"); +const { gitTag } = require("@lerna-test/git-tag"); +const { gitCommit } = require("@lerna-test/git-commit"); + +// test command +const { PublishCommand } = require("../index"); +const lernaPublish = require("@lerna-test/command-runner")(require("../../../cli/src/cli-commands/cli-publish-commands")); + +const yargParser = require('yargs-parser'); + +const createArgv = (cwd, ...args) => { + args.unshift('publish'); + const parserArgs = args.join(' '); + const argv = yargParser(parserArgs); + argv['$0'] = cwd; + return argv; +}; + +describe("workspace protocol 'workspace:' specifiers", () => { + const setupChanges = async (cwd, pkgRoot = "packages") => { + await fs.outputFile(path.join(cwd, `${pkgRoot}/package-1/hello.js`), "world"); + await gitAdd(cwd, "."); + 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", + }); + + // 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 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", + }); + + // 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", + }); + }); +}); \ No newline at end of file diff --git a/packages/publish/src/publish-command.ts b/packages/publish/src/publish-command.ts index 520daae2..5795d8fc 100644 --- a/packages/publish/src/publish-command.ts +++ b/packages/publish/src/publish-command.ts @@ -8,20 +8,24 @@ import semver from 'semver'; import { VersionCommand } from '@lerna-lite/version'; import { collectUpdates, - createRunner, Command, + CommandType, + Conf, + createRunner, describeRef, getOneTimePassword, logOutput, + NpaResolveResult, npmConf, + OneTimePasswordCache, Package, - promptConfirmation, + PackageGraphNode, prereleaseIdFromVersion, + promptConfirmation, pulseTillDone, runTopologically, throwIfUncommitted, ValidationError, - PackageGraphNode, } from '@lerna-lite/core'; import { getCurrentTags } from './lib/get-current-tags'; import { getTaggedPackages } from './lib/get-tagged-packages'; @@ -46,9 +50,9 @@ export function factory(argv) { export class PublishCommand extends Command { /** command name */ - name = 'publish'; - conf: any; - otpCache: any; + name = 'publish' as CommandType; + conf!: Conf & { snapshot?: any; }; + otpCache!: OneTimePasswordCache; gitReset = false; savePrefix = ''; tagPrefix = ''; @@ -56,11 +60,11 @@ export class PublishCommand extends Command { npmSession = ''; packagesToPublish: Package[] = []; packagesToBeLicensed?: Package[] = []; - runPackageLifecycle: any; + runPackageLifecycle!: (pkg: Package, stage: string) => Promise; runRootLifecycle!: (stage: string) => Promise | void; verifyAccess = false; twoFactorAuthRequired = false; - updates: any[] = []; + updates: PackageGraphNode[] = []; updatesVersions?: Map; get otherCommandConfigs() { @@ -183,7 +187,7 @@ export class PublishCommand extends Command { chain = chain.then(() => new VersionCommand(this.argv)); } - return chain.then((result: { updates: Package[]; updatesVersions: Map; needsConfirmation: boolean; }) => { + return chain.then((result: { updates: PackageGraphNode[]; updatesVersions: Map; needsConfirmation: boolean; }) => { if (!result) { // early return from nested VersionCommand return false; @@ -232,6 +236,7 @@ export class PublishCommand extends Command { } await this.resolveLocalDependencyLinks(); + await this.resolveLocalDependencyWorkspaces(); await this.annotateGitHead(); await this.serializeChanges(); await this.packUpdated(); @@ -533,7 +538,7 @@ export class PublishCommand extends Command { const depVersion = this.updatesVersions?.get(depName) || this.packageGraph?.get(depName).pkg.version; // it no longer matters if we mutate the shared Package instance - node.pkg.updateLocalDependency(resolved, depVersion, this.savePrefix); + node.pkg.updateLocalDependency(resolved, depVersion, this.savePrefix, this.commandName); } // writing changes to disk handled in serializeChanges() @@ -543,7 +548,7 @@ export class PublishCommand extends Command { resolveLocalDependencyLinks() { // resolve relative file: links to their actual version range const updatesWithLocalLinks = this.updates.filter((node: PackageGraphNode) => - Array.from(node.localDependencies.values()).some((resolved: any) => resolved.type === 'directory') + Array.from(node.localDependencies.values()).some((resolved: NpaResolveResult) => resolved.type === 'directory') ); return pMap(updatesWithLocalLinks, (node: PackageGraphNode) => { @@ -552,7 +557,26 @@ export class PublishCommand extends Command { const depVersion = this.updatesVersions?.get(depName) || this.packageGraph?.get(depName).pkg.version; // it no longer matters if we mutate the shared Package instance - node.pkg.updateLocalDependency(resolved, depVersion, this.savePrefix); + node.pkg.updateLocalDependency(resolved, depVersion, this.savePrefix, this.commandName); + } + + // writing changes to disk handled in serializeChanges() + }); + } + + resolveLocalDependencyWorkspaces() { + // resolve workspace protocol: translates to their actual version target/range + const updatesWithLocalWorkspaces = this.updates.filter((node: PackageGraphNode) => + Array.from(node.localDependencies.values()).some((resolved: NpaResolveResult) => resolved.explicitWorkspace) + ); + + return pMap(updatesWithLocalWorkspaces, (node: PackageGraphNode) => { + for (const [depName, resolved] of node.localDependencies) { + // regardless of where the version comes from, we can't publish 'workspace:*' specs + const depVersion = this.updatesVersions?.get(depName) || this.packageGraph?.get(depName).pkg.version; + + // it no longer matters if we mutate the shared Package instance + node.pkg.updateLocalDependency(resolved, depVersion, this.savePrefix, this.commandName); } // writing changes to disk handled in serializeChanges() @@ -660,7 +684,6 @@ export class PublishCommand extends Command { packUpdated() { const tracker = this.logger.newItem('npm pack'); - tracker.addWork(this.packagesToPublish?.length); let chain: Promise = Promise.resolve(); @@ -682,8 +705,8 @@ export class PublishCommand extends Command { ...[ this.options.requireScripts && ((pkg: Package) => this.execScript(pkg, 'prepublish')), - (pkg: any) => - pulseTillDone(packDirectory(pkg, pkg.location, opts)).then((packed: any) => { + (pkg: Package & { packed: Tarball; }) => + pulseTillDone(packDirectory(pkg, pkg.location, opts)).then((packed: Tarball) => { tracker.verbose('packed', path.relative(this.project.rootPath ?? '', pkg.contents)); tracker.completeWork(1); @@ -712,7 +735,6 @@ export class PublishCommand extends Command { publishPacked() { const tracker = this.logger.newItem('publish'); - tracker.addWork(this.packagesToPublish?.length); let chain: Promise = Promise.resolve(); diff --git a/packages/run/src/run-command.ts b/packages/run/src/run-command.ts index c9b92492..eb884799 100644 --- a/packages/run/src/run-command.ts +++ b/packages/run/src/run-command.ts @@ -1,5 +1,6 @@ import { Command, + CommandType, logOutput, Package, runTopologically, @@ -17,7 +18,7 @@ export function factory(argv) { export class RunCommand extends Command { /** command name */ - name = 'run'; + name = 'run' as CommandType; args: string[] = []; bail = false; diff --git a/packages/version/src/version-command.ts b/packages/version/src/version-command.ts index b5c5f51f..72c27acf 100644 --- a/packages/version/src/version-command.ts +++ b/packages/version/src/version-command.ts @@ -13,6 +13,7 @@ import { collectPackages, collectUpdates, Command, + CommandType, createRunner, logOutput, Package, @@ -51,7 +52,7 @@ export function factory(argv) { export class VersionCommand extends Command { /** command name */ - name = 'version'; + name = 'version' as CommandType; globalVersion = ''; packagesToVersion: Package[] = []; @@ -541,7 +542,7 @@ export class VersionCommand extends Command { if (depVersion && resolved.type !== 'directory') { // don't overwrite local file: specifiers, they only change during publish - pkg.updateLocalDependency(resolved, depVersion, this.savePrefix); + pkg.updateLocalDependency(resolved, depVersion, this.savePrefix, this.commandName); } }