@@ -6,11 +6,13 @@ import { initMockLogs } from '../__fixtures__/mockLogs';
66import type { Repository } from '../__fixtures__/repository' ;
77import { RepositoryFactory } from '../__fixtures__/repositoryFactory' ;
88import { publish } from '../commands/publish' ;
9- import type { RepoOptions } from '../types/BeachballOptions' ;
9+ import type { ParsedOptions , RepoOptions } from '../types/BeachballOptions' ;
1010import { initNpmMock } from '../__fixtures__/mockNpm' ;
1111import { removeTempDir , tmpdir } from '../__fixtures__/tmpdir' ;
1212import { getParsedOptions } from '../options/getOptions' ;
1313import { createCommandContext } from '../monorepo/createCommandContext' ;
14+ import { validate } from '../validation/validate' ;
15+ import { deepFreezeProperties } from '../__fixtures__/object' ;
1416
1517// Spawning actual npm to run commands against a fake registry is extremely slow, so mock it for
1618// this test (packagePublish covers the more complete npm registry scenario).
@@ -27,9 +29,11 @@ describe('publish command (registry)', () => {
2729 let repo : Repository | undefined ;
2830 let packToPath : string | undefined ;
2931
30- // show error logs for these tests
31- const logs = initMockLogs ( { alsoLog : [ 'error' ] } ) ;
32+ const logs = initMockLogs ( ) ;
3233
34+ /**
35+ * Get options with defaults including skipping git stuff
36+ */
3337 function getOptions ( repoOptions ?: Partial < RepoOptions > ) {
3438 const parsedOptions = getParsedOptions ( {
3539 cwd : repo ! . rootPath ,
@@ -38,6 +42,7 @@ describe('publish command (registry)', () => {
3842 branch : defaultRemoteBranchName ,
3943 registry : 'fake' ,
4044 message : 'apply package updates' ,
45+ fetch : false ,
4146 bumpDeps : false ,
4247 push : false ,
4348 gitTags : false ,
@@ -49,6 +54,20 @@ describe('publish command (registry)', () => {
4954 return { options : parsedOptions . options , parsedOptions } ;
5055 }
5156
57+ /**
58+ * For more realistic testing, call `validate()` like the CLI command does, then call `publish()`.
59+ * This helps catch any new issues with double bumps or context mutation.
60+ */
61+ async function publishWrapper ( parsedOptions : ParsedOptions ) {
62+ logs . clear ( ) ;
63+ // This does an initial bump
64+ const { context } = validate ( parsedOptions , { checkDependencies : true } ) ;
65+ // Ensure the later bump process does not modify the context
66+ deepFreezeProperties ( context . bumpInfo ) ;
67+ deepFreezeProperties ( context . originalPackageInfos ) ;
68+ await publish ( parsedOptions . options , context ) ;
69+ }
70+
5271 afterEach ( ( ) => {
5372 repositoryFactory ?. cleanUp ( ) ;
5473 repositoryFactory = undefined ;
@@ -64,15 +83,14 @@ describe('publish command (registry)', () => {
6483
6584 const { options, parsedOptions } = getOptions ( { packToPath } ) ;
6685 generateChangeFiles ( [ 'foo' ] , options ) ;
67- repo . push ( ) ;
68-
69- await publish ( options , createCommandContext ( parsedOptions ) ) ;
86+ await publishWrapper ( parsedOptions ) ;
7087
7188 expect ( fs . readdirSync ( packToPath ) ) . toEqual ( [ '1-foo-1.1.0.tgz' ] ) ;
7289 expect ( npmMock . getPublishedVersions ( 'foo' ) ) . toBeUndefined ( ) ;
90+ expect ( logs . mocks . error ) . not . toHaveBeenCalled ( ) ;
7391 } ) ;
7492
75- it ( 'publishes in monorepo with mixed public and private packages ' , async ( ) => {
93+ it ( 'skips publishing private package with change file ' , async ( ) => {
7694 repositoryFactory = new RepositoryFactory ( {
7795 folders : {
7896 packages : {
@@ -86,36 +104,92 @@ describe('publish command (registry)', () => {
86104 const { options, parsedOptions } = getOptions ( ) ;
87105 generateChangeFiles ( [ 'foopkg' ] , options ) ;
88106
89- repo . push ( ) ;
90-
91- await publish ( options , createCommandContext ( parsedOptions ) ) ;
92-
107+ // If there's only the private package with a change file, nothing happens
108+ await publishWrapper ( parsedOptions ) ;
93109 expect ( logs . mocks . log ) . toHaveBeenCalledWith ( 'Nothing to bump, skipping publish!' ) ;
94110 expect ( logs . mocks . warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'Change detected for private package foopkg' ) ) ;
95-
111+ expect ( logs . mocks . error ) . not . toHaveBeenCalled ( ) ;
96112 expect ( npmMock . getPublishedVersions ( 'foopkg' ) ) . toBeUndefined ( ) ;
113+
114+ // Now try with a public package change file
115+ logs . clear ( ) ;
116+ generateChangeFiles ( [ 'publicpkg' ] , options ) ;
117+ await publishWrapper ( parsedOptions ) ;
118+ // Should be published despite private package change also existing
119+ expect ( npmMock . getPublishedPackage ( 'publicpkg' ) ! . version ) . toEqual ( '1.1.0' ) ;
120+ // This is also a good case to get a "visual regression" test of the logs
121+ expect ( logs . getMockLines ( 'all' , { root : repo . rootPath , sanitize : true } ) ) . toMatchInlineSnapshot ( `
122+ "[log]
123+ Validating options and change files...
124+ [warn] Change detected for private package foopkg; delete this file: <root>/change/foopkg-<guid>.json
125+ [log]
126+ Validating package dependencies...
127+ [log] Validating no private package among package dependencies
128+ [log] OK!
129+
130+ [log]
131+ [log]
132+ Preparing to publish
133+ [log]
134+ Publishing with the following configuration:
135+
136+ registry: fake
137+
138+ current branch: master
139+ current hash: <commit>
140+ target branch: origin/master
141+ npm dist-tag: latest
142+
143+ bumps versions before publishing: yes
144+ publishes to npm registry: yes
145+ pushes bumps and changelogs to remote git repo: no
146+
147+
148+ [log] Creating temporary publish branch publish_<timestamp>
149+ [log]
150+ Bumping versions and publishing packages to npm registry
151+
152+ [log] Removing change files:
153+ [log] - publicpkg-<guid>.json
154+ [log]
155+ Validating new package versions...
156+ [log]
157+ Package versions are OK to publish:
158+ • publicpkg@1.1.0
159+ [log] Validating no private package among package dependencies
160+ [log] OK!
161+
162+ [log]
163+ Publishing - publicpkg@1.1.0 with tag latest
164+ [log] publish command: publish --registry fake --tag latest --loglevel warn
165+ [log] (cwd: <root>/packages/publicpkg)
166+ [log] Published! - publicpkg@1.1.0
167+ [log]
168+ [log] Skipping git push and tagging
169+ [log]
170+ Cleaning up
171+ [log] git checkout master
172+ [log] deleting temporary publish branch publish_<timestamp>"
173+ ` ) ;
97174 } ) ;
98175
99176 it ( 'publishes multiple changed packages' , async ( ) => {
100- repositoryFactory = new RepositoryFactory ( {
101- folders : {
102- packages : {
103- foopkg : { version : '1.0.0' , dependencies : { barpkg : '^1.0.0' } } ,
104- barpkg : { version : '1.0.0' } ,
105- } ,
106- } ,
107- } ) ;
177+ repositoryFactory = new RepositoryFactory ( 'monorepo' ) ;
108178 repo = repositoryFactory . cloneRepository ( ) ;
179+ // Simulate the current package versions already existing to test validatePackageVersions
180+ npmMock . publishPackage ( repositoryFactory . fixture . folders . packages . foo ) ;
181+ npmMock . publishPackage ( repositoryFactory . fixture . folders . packages . bar ) ;
182+ npmMock . publishPackage ( repositoryFactory . fixture . folders . packages . baz ) ;
109183
110184 const { options, parsedOptions } = getOptions ( ) ;
111- generateChangeFiles ( [ 'foopkg ' , 'barpkg ' ] , options ) ;
185+ generateChangeFiles ( [ 'foo ' , 'bar ' ] , options ) ;
112186
113- repo . push ( ) ;
187+ await publishWrapper ( parsedOptions ) ;
114188
115- await publish ( options , createCommandContext ( parsedOptions ) ) ;
116-
117- expect ( npmMock . getPublishedPackage ( 'foopkg' ) ! . version ) . toEqual ( '1.1.0' ) ;
118- expect ( npmMock . getPublishedPackage ( 'barpkg' ) ! . version ) . toEqual ( '1.1.0' ) ;
189+ expect ( npmMock . getPublishedPackage ( 'foo' ) ! . version ) . toEqual ( '1.1.0' ) ;
190+ expect ( npmMock . getPublishedPackage ( 'bar' ) ! . version ) . toEqual ( '1.4.0' ) ;
191+ expect ( npmMock . mock ) . toHaveBeenCalledTimes ( 2 ) ;
192+ expect ( logs . mocks . error ) . not . toHaveBeenCalled ( ) ;
119193 } ) ;
120194
121195 it ( 'packs many packages' , async ( ) => {
@@ -136,14 +210,14 @@ describe('publish command (registry)', () => {
136210
137211 const { options, parsedOptions } = getOptions ( { packToPath, groupChanges : true } ) ;
138212 generateChangeFiles ( packageNames , options ) ;
139- repo . push ( ) ;
140-
213+ // initial validate() isn't relevant here
141214 await publish ( options , createCommandContext ( parsedOptions ) ) ;
142215
143216 expect ( fs . readdirSync ( packToPath ) . sort ( ) ) . toEqual (
144217 [ ...packageNames ] . reverse ( ) . map ( ( name , i ) => `${ String ( i + 1 ) . padStart ( 2 , '0' ) } -${ name } -1.1.0.tgz` )
145218 ) ;
146219 expect ( npmMock . getPublishedVersions ( 'pkg-1' ) ) . toBeUndefined ( ) ;
220+ expect ( logs . mocks . error ) . not . toHaveBeenCalled ( ) ;
147221 } ) ;
148222
149223 it ( 'exits publishing early if only invalid change files exist' , async ( ) => {
@@ -155,16 +229,43 @@ describe('publish command (registry)', () => {
155229 const { options, parsedOptions } = getOptions ( ) ;
156230 generateChangeFiles ( [ 'bar' , 'fake' ] , options ) ;
157231
158- repo . push ( ) ;
159-
232+ // initial validate() isn't relevant here
160233 await publish ( options , createCommandContext ( parsedOptions ) ) ;
161234
162235 expect ( logs . mocks . log ) . toHaveBeenCalledWith ( 'Nothing to bump, skipping publish!' ) ;
163236 expect ( logs . mocks . warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'Change detected for private package bar' ) ) ;
164237 expect ( logs . mocks . warn ) . toHaveBeenCalledWith (
165238 expect . stringContaining ( 'Change detected for nonexistent package fake' )
166239 ) ;
240+ expect ( logs . mocks . error ) . not . toHaveBeenCalled ( ) ;
167241
168242 expect ( npmMock . getPublishedVersions ( 'foo' ) ) . toBeUndefined ( ) ;
169243 } ) ;
244+
245+ it ( 'errors if version already exists in registry' , async ( ) => {
246+ repositoryFactory = new RepositoryFactory ( 'monorepo' ) ;
247+ repo = repositoryFactory . cloneRepository ( ) ;
248+ // Simulate the current package versions already existing to test validatePackageVersions
249+ npmMock . publishPackage ( repositoryFactory . fixture . folders . packages . foo ) ;
250+ npmMock . publishPackage ( repositoryFactory . fixture . folders . packages . bar ) ;
251+ npmMock . publishPackage ( repositoryFactory . fixture . folders . packages . baz ) ;
252+ // also say the bumped version of foo and bar already exist (baz is fine)
253+ npmMock . publishPackage ( { ...repositoryFactory . fixture . folders . packages . foo , version : '1.1.0' } ) ;
254+ npmMock . publishPackage ( { ...repositoryFactory . fixture . folders . packages . bar , version : '1.4.0' } ) ;
255+
256+ const { options, parsedOptions } = getOptions ( ) ;
257+ generateChangeFiles ( [ 'foo' , 'bar' , 'baz' ] , options ) ;
258+ await expect ( ( ) => publishWrapper ( parsedOptions ) ) . rejects . toThrow ( 'process.exit' ) ;
259+
260+ expect ( logs . getMockLines ( 'error' ) ) . toMatchInlineSnapshot ( `
261+ "ERROR: Attempting to publish package versions that already exist in the registry:
262+ • bar@1.4.0
263+ • foo@1.1.0
264+ Something went wrong with publishing! Manually update these package and versions:
265+ • bar@1.4.0
266+ • baz@1.4.0
267+ • foo@1.1.0
268+ No packages were published due to validation errors (see above for details)."
269+ ` ) ;
270+ } ) ;
170271} ) ;
0 commit comments