@ckeditor/ckeditor5-dev-release-tools 44.0.0 → 44.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -33,3 +33,4 @@ export { default as verifyPackagesPublishedCorrectly } from './tasks/verifypacka
33
33
  export { default as getNpmTagFromVersion } from './utils/getnpmtagfromversion.js';
34
34
  export { default as isVersionPublishableForTag } from './utils/isversionpublishablefortag.js';
35
35
  export { default as provideToken } from './utils/providetoken.js';
36
+ export { default as findPathsToPackages } from './utils/findpathstopackages.js';
@@ -6,6 +6,7 @@
6
6
  import fs from 'fs-extra';
7
7
  import upath from 'upath';
8
8
  import { glob } from 'glob';
9
+ import findPathsToPackages from '../utils/findpathstopackages.js';
9
10
 
10
11
  /**
11
12
  * The purpose of the script is to clean all packages prepared for the release. The cleaning consists of two stages:
@@ -29,12 +30,7 @@ import { glob } from 'glob';
29
30
  */
30
31
  export default async function cleanUpPackages( options ) {
31
32
  const { packagesDirectory, packageJsonFieldsToRemove, preservePostInstallHook, cwd } = parseOptions( options );
32
-
33
- const packageJsonPaths = await glob( '*/package.json', {
34
- cwd: upath.join( cwd, packagesDirectory ),
35
- nodir: true,
36
- absolute: true
37
- } );
33
+ const packageJsonPaths = await findPathsToPackages( cwd, packagesDirectory, { includePackageJson: true } );
38
34
 
39
35
  for ( const packageJsonPath of packageJsonPaths ) {
40
36
  const packagePath = upath.dirname( packageJsonPath );
@@ -4,9 +4,8 @@
4
4
  */
5
5
 
6
6
  import upath from 'upath';
7
- import { tools } from '@ckeditor/ckeditor5-dev-utils';
8
7
  import { glob } from 'glob';
9
- import shellEscape from 'shell-escape';
8
+ import { simpleGit } from 'simple-git';
10
9
 
11
10
  const { toUnix } = upath;
12
11
 
@@ -27,22 +26,18 @@ export default async function commitAndTag( { version, files, cwd = process.cwd(
27
26
  return;
28
27
  }
29
28
 
30
- const shExecOptions = {
31
- cwd: normalizedCwd,
32
- async: true,
33
- verbosity: 'silent'
34
- };
29
+ const git = simpleGit( {
30
+ baseDir: normalizedCwd
31
+ } );
35
32
 
36
- // Run the command separately for each file to avoid exceeding the maximum command length on Windows, which is 32767 characters.
37
- for ( const filePath of filePathsToAdd ) {
38
- await tools.shExec( `git add ${ shellEscape( [ filePath ] ) }`, shExecOptions );
39
- }
33
+ const { all: availableTags } = await git.tags();
34
+ const tagForVersion = availableTags.find( tag => tag.endsWith( version ) );
40
35
 
41
- const escapedVersion = {
42
- commit: shellEscape( [ `Release: v${ version }.` ] ),
43
- tag: shellEscape( [ `v${ version }` ] )
44
- };
36
+ // Do not commit and create tags if a tag is already taken. It might happen when a release job is restarted.
37
+ if ( tagForVersion ) {
38
+ return;
39
+ }
45
40
 
46
- await tools.shExec( `git commit --message ${ escapedVersion.commit } --no-verify`, shExecOptions );
47
- await tools.shExec( `git tag ${ escapedVersion.tag }`, shExecOptions );
41
+ await git.commit( `Release: v${ version }.`, filePathsToAdd );
42
+ await git.addTag( `v${ version }` );
48
43
  }
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import { Octokit } from '@octokit/rest';
7
- import semver from 'semver';
8
7
  import * as transformCommitUtils from '../utils/transformcommitutils.js';
8
+ import getNpmTagFromVersion from '../utils/getnpmtagfromversion.js';
9
9
 
10
10
  const { getRepositoryUrl } = transformCommitUtils;
11
11
 
@@ -41,25 +41,13 @@ export default async function createGithubRelease( options ) {
41
41
  owner: repositoryOwner,
42
42
  repo: repositoryName,
43
43
  body: description,
44
- prerelease: getVersionTag( version ) !== 'latest'
44
+ prerelease: getNpmTagFromVersion( version ) !== 'latest'
45
45
  } );
46
46
  }
47
47
 
48
48
  return `https://github.com/${ repositoryOwner }/${ repositoryName }/releases/tag/v${ version }`;
49
49
  }
50
50
 
51
- /**
52
- * Returns an npm tag based on the specified release version.
53
- *
54
- * @param {string} version
55
- * @returns {string}
56
- */
57
- function getVersionTag( version ) {
58
- const [ versionTag ] = semver.prerelease( version ) || [ 'latest' ];
59
-
60
- return versionTag;
61
- }
62
-
63
51
  /**
64
52
  * Resolves a promise containing a flag if the GitHub contains the release page for given version.
65
53
  *
@@ -301,7 +301,14 @@ export default async function generateChangelogForMonoRepository( options ) {
301
301
  bumpType = 'patch';
302
302
  }
303
303
 
304
- return provideNewVersionForMonoRepository( highestVersion, packageHighestVersion, bumpType, { indentLevel: 1 } )
304
+ const provideVersionOptions = {
305
+ packageName: packageHighestVersion,
306
+ version: highestVersion,
307
+ bumpType,
308
+ indentLevel: 1
309
+ };
310
+
311
+ return provideNewVersionForMonoRepository( provideVersionOptions )
305
312
  .then( version => {
306
313
  nextVersion = version;
307
314
 
@@ -79,11 +79,14 @@ export default async function generateChangelogForSinglePackage( options = {} )
79
79
  .then( () => {
80
80
  logProcess( 'Preparing new version for the package...' );
81
81
 
82
- const releaseType = getNewVersionType( allCommits );
83
-
84
82
  displayCommits( allCommits, { indentLevel: 1 } );
85
83
 
86
- return provideVersion( pkgJson.version, releaseType, { indentLevel: 1 } );
84
+ return provideVersion( {
85
+ packageName: pkgJson.name,
86
+ version: pkgJson.version,
87
+ indentLevel: 1,
88
+ releaseTypeOrNewVersion: getNewVersionType( allCommits )
89
+ } );
87
90
  } )
88
91
  .then( version => {
89
92
  if ( version === 'skip' ) {
@@ -4,44 +4,50 @@
4
4
  */
5
5
 
6
6
  import upath from 'upath';
7
- import { glob } from 'glob';
7
+ import fs from 'fs-extra';
8
8
  import assertNpmAuthorization from '../utils/assertnpmauthorization.js';
9
9
  import assertPackages from '../utils/assertpackages.js';
10
10
  import assertNpmTag from '../utils/assertnpmtag.js';
11
11
  import assertFilesToPublish from '../utils/assertfilestopublish.js';
12
12
  import executeInParallel from '../utils/executeinparallel.js';
13
13
  import publishPackageOnNpmCallback from '../utils/publishpackageonnpmcallback.js';
14
+ import checkVersionAvailability from '../utils/checkversionavailability.js';
15
+ import findPathsToPackages from '../utils/findpathstopackages.js';
14
16
 
15
17
  /**
16
18
  * The purpose of the script is to validate the packages prepared for the release and then release them on npm.
17
19
  *
18
20
  * The validation contains the following steps in each package:
19
21
  * - User must be logged to npm on the specified account.
20
- * - The package directory mmust contain `package.json` file.
22
+ * - The package directory must contain `package.json` file.
21
23
  * - All other files expected to be released must exist in the package directory.
22
24
  * - The npm tag must match the tag calculated from the package version.
23
25
  *
24
26
  * When the validation for each package passes, packages are published on npm. Optional callback is called for confirmation whether to
25
27
  * continue.
26
28
  *
29
+ * If a package has already been published, the script does not try to publish it again. Instead, it treats the package as published.
30
+ * Whenever a communication between the script and npm fails, it tries to re-publish a package (up to three attempts).
31
+ *
27
32
  * @param {object} options
28
33
  * @param {string} options.packagesDirectory Relative path to a location of packages to release.
29
34
  * @param {string} options.npmOwner The account name on npm, which should be used to publish the packages.
30
- * @param {ListrTaskObject} options.listrTask An instance of `ListrTask`.
35
+ * @param {ListrTaskObject} [options.listrTask] An instance of `ListrTask`.
31
36
  * @param {AbortSignal|null} [options.signal=null] Signal to abort the asynchronous process.
32
37
  * @param {string} [options.npmTag='staging'] The npm distribution tag.
33
38
  * @param {Object.<string, Array.<string>>|null} [options.optionalEntries=null] Specifies which entries from the `files` field in the
34
39
  * `package.json` are optional. The key is a package name, and its value is an array of optional entries from the `files` field, for which
35
40
  * it is allowed not to match any file. The `options.optionalEntries` object may also contain the `default` key, which is used for all
36
41
  * packages that do not have own definition.
37
- * @param {string} [options.confirmationCallback=null] An callback whose response decides to continue the publishing packages. Synchronous
38
- * and asynchronous callbacks are supported.
42
+ * @param {function|null} [options.confirmationCallback=null] An callback whose response decides to continue the publishing packages.
43
+ * Synchronous and asynchronous callbacks are supported.
39
44
  * @param {boolean} [options.requireEntryPoint=false] Whether to verify if packages to publish define an entry point. In other words,
40
45
  * whether their `package.json` define the `main` field.
41
46
  * @param {Array.<string>} [options.optionalEntryPointPackages=[]] If the entry point validator is enabled (`requireEntryPoint=true`),
42
47
  * this array contains a list of packages that will not be checked. In other words, they do not have to define the entry point.
43
48
  * @param {string} [options.cwd=process.cwd()] Current working directory from which all paths will be resolved.
44
49
  * @param {number} [options.concurrency=4] Number of CPUs that will execute the task.
50
+ * @param {number} [options.attempts=3] Number of attempts. After reaching 0, it won't be publishing packages again.
45
51
  * @returns {Promise}
46
52
  */
47
53
  export default async function publishPackages( options ) {
@@ -56,15 +62,14 @@ export default async function publishPackages( options ) {
56
62
  requireEntryPoint = false,
57
63
  optionalEntryPointPackages = [],
58
64
  cwd = process.cwd(),
59
- concurrency = 4
65
+ concurrency = 4,
66
+ attempts = 3
60
67
  } = options;
61
68
 
69
+ const remainingAttempts = attempts - 1;
62
70
  await assertNpmAuthorization( npmOwner );
63
71
 
64
- const packagePaths = await glob( '*/', {
65
- cwd: upath.join( cwd, packagesDirectory ),
66
- absolute: true
67
- } );
72
+ const packagePaths = await findPathsToPackages( cwd, packagesDirectory );
68
73
 
69
74
  await assertPackages( packagePaths, { requireEntryPoint, optionalEntryPointPackages } );
70
75
  await assertFilesToPublish( packagePaths, optionalEntries );
@@ -72,17 +77,69 @@ export default async function publishPackages( options ) {
72
77
 
73
78
  const shouldPublishPackages = confirmationCallback ? await confirmationCallback() : true;
74
79
 
75
- if ( shouldPublishPackages ) {
76
- await executeInParallel( {
77
- cwd,
78
- packagesDirectory,
79
- listrTask,
80
- taskToExecute: publishPackageOnNpmCallback,
81
- taskOptions: {
82
- npmTag
83
- },
84
- signal,
85
- concurrency
86
- } );
80
+ if ( !shouldPublishPackages ) {
81
+ return Promise.resolve();
87
82
  }
83
+
84
+ await removeAlreadyPublishedPackages( packagePaths );
85
+
86
+ await executeInParallel( {
87
+ cwd,
88
+ packagesDirectory,
89
+ listrTask,
90
+ taskToExecute: publishPackageOnNpmCallback,
91
+ taskOptions: {
92
+ npmTag
93
+ },
94
+ signal,
95
+ concurrency
96
+ } );
97
+
98
+ const packagePathsAfterPublishing = await findPathsToPackages( cwd, packagesDirectory );
99
+
100
+ // All packages have been published. No need for re-executing.
101
+ if ( !packagePathsAfterPublishing.length ) {
102
+ return Promise.resolve();
103
+ }
104
+
105
+ // No more attempts. Abort.
106
+ if ( remainingAttempts <= 0 ) {
107
+ throw new Error( 'Some packages could not be published.' );
108
+ }
109
+
110
+ // Let's give an npm a moment for taking a breath...
111
+ await wait( 1000 );
112
+
113
+ // ...and try again.
114
+ return publishPackages( {
115
+ packagesDirectory,
116
+ npmOwner,
117
+ listrTask,
118
+ signal,
119
+ npmTag,
120
+ optionalEntries,
121
+ requireEntryPoint,
122
+ optionalEntryPointPackages,
123
+ cwd,
124
+ concurrency,
125
+ confirmationCallback: null, // Do not ask again if already here.
126
+ attempts: remainingAttempts
127
+ } );
128
+ }
129
+
130
+ async function removeAlreadyPublishedPackages( packagePaths ) {
131
+ for ( const absolutePackagePath of packagePaths ) {
132
+ const pkgJson = await fs.readJson( upath.join( absolutePackagePath, 'package.json' ) );
133
+ const isAvailable = await checkVersionAvailability( pkgJson.version, pkgJson.name );
134
+
135
+ if ( !isAvailable ) {
136
+ await fs.remove( absolutePackagePath );
137
+ }
138
+ }
139
+ }
140
+
141
+ function wait( time ) {
142
+ return new Promise( resolve => {
143
+ setTimeout( resolve, time );
144
+ } );
88
145
  }
@@ -4,8 +4,10 @@
4
4
  */
5
5
 
6
6
  import fs from 'fs-extra';
7
- import { glob } from 'glob';
8
7
  import upath from 'upath';
8
+ import findPathsToPackages from '../utils/findpathstopackages.js';
9
+
10
+ const { normalizeTrim } = upath;
9
11
 
10
12
  /**
11
13
  * The purpose of this script is to update all eligible dependencies to a version specified in the `options.version`. The following packages
@@ -38,15 +40,15 @@ export default async function updateDependencies( options ) {
38
40
  cwd = process.cwd()
39
41
  } = options;
40
42
 
41
- const globPatterns = [ 'package.json' ];
42
-
43
- if ( packagesDirectory ) {
44
- const packagesDirectoryPattern = upath.join( packagesDirectory, '*', 'package.json' );
45
-
46
- globPatterns.push( packagesDirectoryPattern );
47
- }
48
-
49
- const pkgJsonPaths = await getPackageJsonPaths( cwd, globPatterns, packagesDirectoryFilter );
43
+ const pkgJsonPaths = await findPathsToPackages(
44
+ cwd,
45
+ packagesDirectory ? normalizeTrim( packagesDirectory ) : null,
46
+ {
47
+ includePackageJson: true,
48
+ includeCwd: true,
49
+ packagesDirectoryFilter
50
+ }
51
+ );
50
52
 
51
53
  for ( const pkgJsonPath of pkgJsonPaths ) {
52
54
  const pkgJson = await fs.readJson( pkgJsonPath );
@@ -78,28 +80,6 @@ function updateVersion( version, callback, dependencies ) {
78
80
  }
79
81
  }
80
82
 
81
- /**
82
- * @param {string} cwd
83
- * @param {Array.<string>} globPatterns
84
- * @param {UpdateDependenciesPackagesDirectoryFilter|null} packagesDirectoryFilter
85
- * @returns {Promise.<Array.<string>>}
86
- */
87
- async function getPackageJsonPaths( cwd, globPatterns, packagesDirectoryFilter ) {
88
- const globOptions = {
89
- cwd,
90
- nodir: true,
91
- absolute: true
92
- };
93
-
94
- const pkgJsonPaths = await glob( globPatterns, globOptions );
95
-
96
- if ( !packagesDirectoryFilter ) {
97
- return pkgJsonPaths;
98
- }
99
-
100
- return pkgJsonPaths.filter( packagesDirectoryFilter );
101
- }
102
-
103
83
  /**
104
84
  * @callback UpdateVersionCallback
105
85
  *
@@ -5,11 +5,10 @@
5
5
 
6
6
  import upath from 'upath';
7
7
  import fs from 'fs-extra';
8
- import { glob } from 'glob';
9
8
  import semver from 'semver';
10
- import checkVersionAvailability from '../utils/checkversionavailability.js';
9
+ import findPathsToPackages from '../utils/findpathstopackages.js';
11
10
 
12
- const { normalizeTrim, toUnix, dirname, join } = upath;
11
+ const { normalizeTrim } = upath;
13
12
 
14
13
  /**
15
14
  * The purpose of the script is to update the version of a root package found in the current working
@@ -38,24 +37,18 @@ export default async function updateVersions( options ) {
38
37
  packagesDirectoryFilter = null,
39
38
  cwd = process.cwd()
40
39
  } = options;
41
- const normalizedCwd = toUnix( cwd );
42
- const normalizedPackagesDir = packagesDirectory ? normalizeTrim( packagesDirectory ) : null;
43
40
 
44
- const globPatterns = getGlobPatterns( normalizedPackagesDir );
45
- const pkgJsonPaths = await getPackageJsonPaths( cwd, globPatterns, packagesDirectoryFilter );
46
-
47
- const randomPackagePath = getRandomPackagePath( pkgJsonPaths, normalizedPackagesDir );
48
-
49
- const { version: rootPackageVersion } = await readPackageJson( normalizedCwd );
50
- const { name: randomPackageName } = await readPackageJson( randomPackagePath );
51
-
52
- checkIfVersionIsValid( version, rootPackageVersion );
41
+ const pkgJsonPaths = await findPathsToPackages(
42
+ cwd,
43
+ packagesDirectory ? normalizeTrim( packagesDirectory ) : null,
44
+ {
45
+ includePackageJson: true,
46
+ includeCwd: true,
47
+ packagesDirectoryFilter
48
+ }
49
+ );
53
50
 
54
- const isVersionAvailable = await checkVersionAvailability( version, randomPackageName );
55
-
56
- if ( !isVersionAvailable ) {
57
- throw new Error( `The "${ randomPackageName }@${ version }" already exists in the npm registry.` );
58
- }
51
+ checkIfVersionIsValid( version );
59
52
 
60
53
  for ( const pkgJsonPath of pkgJsonPaths ) {
61
54
  const pkgJson = await fs.readJson( pkgJsonPath );
@@ -66,78 +59,11 @@ export default async function updateVersions( options ) {
66
59
  }
67
60
 
68
61
  /**
69
- * @param {string} cwd
70
- * @param {Array.<string>} globPatterns
71
- * @param {UpdateVersionsPackagesDirectoryFilter|null} packagesDirectoryFilter
72
- * @returns {Promise.<Array.<string>>}
73
- */
74
- async function getPackageJsonPaths( cwd, globPatterns, packagesDirectoryFilter ) {
75
- const pkgJsonPaths = await glob( globPatterns, {
76
- cwd,
77
- absolute: true,
78
- nodir: true
79
- } );
80
-
81
- if ( !packagesDirectoryFilter ) {
82
- return pkgJsonPaths;
83
- }
84
-
85
- return pkgJsonPaths.filter( packagesDirectoryFilter );
86
- }
87
-
88
- /**
89
- * @param {string} packagesDirectory
90
- * @returns {Promise.<object>}
62
+ * @param {string} version
91
63
  */
92
- function readPackageJson( packagesDirectory ) {
93
- const packageJsonPath = join( packagesDirectory, 'package.json' );
94
-
95
- return fs.readJson( packageJsonPath );
96
- }
97
-
98
- /**
99
- * @param {string|null} packagesDirectory
100
- * @returns {Array.<string>}
101
- */
102
- function getGlobPatterns( packagesDirectory ) {
103
- const patterns = [ 'package.json' ];
104
-
105
- if ( packagesDirectory ) {
106
- patterns.push( packagesDirectory + '/*/package.json' );
107
- }
108
-
109
- return patterns;
110
- }
111
-
112
- /**
113
- * @param {Array.<string>} pkgJsonPaths
114
- * @param {string|null} packagesDirectory
115
- * @returns {object}
116
- */
117
- function getRandomPackagePath( pkgJsonPaths, packagesDirectory ) {
118
- const randomPkgJsonPaths = packagesDirectory ?
119
- pkgJsonPaths.filter( packagePath => packagePath.includes( packagesDirectory ) ) :
120
- pkgJsonPaths;
121
- const randomPkgJsonPath = randomPkgJsonPaths[ Math.floor( Math.random() * randomPkgJsonPaths.length ) ];
122
-
123
- return dirname( randomPkgJsonPath );
124
- }
125
-
126
- /**
127
- * Checks if the specified version is greater than the current one.
128
- *
129
- * A nightly version is always considered as valid.
130
- *
131
- * @param {string} newVersion
132
- * @param {string} currentVersion
133
- */
134
- function checkIfVersionIsValid( newVersion, currentVersion ) {
135
- if ( newVersion.startsWith( '0.0.0-nightly' ) ) {
136
- return;
137
- }
138
-
139
- if ( !semver.gt( newVersion, currentVersion ) ) {
140
- throw new Error( `Provided version ${ newVersion } must be greater than ${ currentVersion } or match pattern 0.0.0-nightly.` );
64
+ function checkIfVersionIsValid( version ) {
65
+ if ( !semver.valid( version ) ) {
66
+ throw new Error( `Provided version ${ version } must follow the "Semantic Versioning" standard.` );
141
67
  }
142
68
  }
143
69
 
@@ -19,6 +19,16 @@ import checkVersionAvailability from '../utils/checkversionavailability.js';
19
19
  * @returns {Promise}
20
20
  */
21
21
  export default async function verifyPackagesPublishedCorrectly( options ) {
22
+ process.emitWarning(
23
+ 'The `verifyPackagesPublishedCorrectly()` function is deprecated and will be removed in the upcoming release (v45). ' +
24
+ 'Its responsibility has been merged with `publishPackages()`.',
25
+ {
26
+ type: 'DeprecationWarning',
27
+ code: 'DEP0001',
28
+ detail: 'https://github.com/ckeditor/ckeditor5-dev/blob/master/DEPRECATIONS.md#dep0001-verifypackagespublishedcorrectly'
29
+ }
30
+ );
31
+
22
32
  const { packagesDirectory, version, onSuccess } = options;
23
33
  const packagesToVerify = await glob( upath.join( packagesDirectory, '*' ), { absolute: true } );
24
34
  const errors = [];
@@ -32,16 +42,12 @@ export default async function verifyPackagesPublishedCorrectly( options ) {
32
42
  for ( const packageToVerify of packagesToVerify ) {
33
43
  const packageJson = await fs.readJson( upath.join( packageToVerify, 'package.json' ) );
34
44
 
35
- try {
36
- const packageWasUploadedCorrectly = !await checkVersionAvailability( version, packageJson.name );
45
+ const isPackageVersionAvailable = await checkVersionAvailability( version, packageJson.name );
37
46
 
38
- if ( packageWasUploadedCorrectly ) {
39
- await fs.remove( packageToVerify );
40
- } else {
41
- errors.push( packageJson.name );
42
- }
43
- } catch {
47
+ if ( isPackageVersionAvailable ) {
44
48
  errors.push( packageJson.name );
49
+ } else {
50
+ await fs.remove( packageToVerify );
45
51
  }
46
52
  }
47
53
 
@@ -5,7 +5,12 @@
5
5
 
6
6
  import fs from 'fs-extra';
7
7
  import upath from 'upath';
8
- import semver from 'semver';
8
+ import getNpmTagFromVersion from './getnpmtagfromversion.js';
9
+
10
+ const ALLOWED_NPM_LATEST_TAGS = [
11
+ 'staging',
12
+ 'next'
13
+ ];
9
14
 
10
15
  /**
11
16
  * Checks if the npm tag matches the tag calculated from the package version. Verification takes place for all packages.
@@ -20,13 +25,13 @@ export default async function assertNpmTag( packagePaths, npmTag ) {
20
25
  for ( const packagePath of packagePaths ) {
21
26
  const packageJsonPath = upath.join( packagePath, 'package.json' );
22
27
  const packageJson = await fs.readJson( packageJsonPath );
23
- const versionTag = getVersionTag( packageJson.version );
28
+ const versionTag = getNpmTagFromVersion( packageJson.version );
24
29
 
25
30
  if ( versionTag === npmTag ) {
26
31
  continue;
27
32
  }
28
33
 
29
- if ( versionTag === 'latest' && npmTag === 'staging' ) {
34
+ if ( versionTag === 'latest' && ALLOWED_NPM_LATEST_TAGS.includes( npmTag ) ) {
30
35
  continue;
31
36
  }
32
37
 
@@ -37,22 +42,3 @@ export default async function assertNpmTag( packagePaths, npmTag ) {
37
42
  throw new Error( errors.join( '\n' ) );
38
43
  }
39
44
  }
40
-
41
- /**
42
- * Returns the version tag for the package.
43
- *
44
- * For the official release, returns the "latest" tag. For a non-official release (pre-release), returns the version tag extracted from
45
- * the package version.
46
- *
47
- * @param {string} version
48
- * @returns {string}
49
- */
50
- function getVersionTag( version ) {
51
- const [ versionTag ] = semver.prerelease( version ) || [ 'latest' ];
52
-
53
- if ( versionTag.startsWith( 'nightly' ) ) {
54
- return 'nightly';
55
- }
56
-
57
- return versionTag;
58
- }
@@ -3,41 +3,25 @@
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
5
 
6
- import { tools } from '@ckeditor/ckeditor5-dev-utils';
7
- import shellEscape from 'shell-escape';
6
+ import pacote from 'pacote';
8
7
 
9
8
  /**
10
9
  * Checks if the provided version for the package exists in the npm registry.
11
10
  *
12
11
  * Returns a promise that resolves to `true` if the provided version does not exist or resolves the promise to `false` otherwise.
13
- * If the `npm show` command exits with an error, it is re-thrown.
14
12
  *
15
13
  * @param {string} version
16
14
  * @param {string} packageName
17
15
  * @returns {Promise}
18
16
  */
19
17
  export default async function checkVersionAvailability( version, packageName ) {
20
- const command = `npm show ${ shellEscape( [ packageName ] ) }@${ shellEscape( [ version ] ) } version`;
21
-
22
- return tools.shExec( command, { verbosity: 'silent', async: true } )
23
- .then( result => {
24
- // Explicit check for npm < 8.13.0, which does not return anything (an empty result) and it exits with a zero status code when
25
- // the version for the provided package does not exist in the npm registry.
26
- if ( !result ) {
27
- return true;
28
- }
29
-
30
- // Provided version exists in the npm registry.
18
+ return pacote.manifest( `${ packageName }@${ version }` )
19
+ .then( () => {
20
+ // If `pacote.manifest` resolves, a package with the given version exists.
31
21
  return false;
32
22
  } )
33
- .catch( error => {
34
- // All errors except the "E404" are re-thrown.
35
- if ( !error.toString().includes( 'code E404' ) ) {
36
- throw error;
37
- }
38
-
39
- // Npm >= 8.13.0 throws an "E404" error if a version does not exist.
40
- // Npm < 8.13.0 should never reach this line.
23
+ .catch( () => {
24
+ // When throws, the package does not exist.
41
25
  return true;
42
26
  } );
43
27
  }
@@ -10,8 +10,8 @@ import upath from 'upath';
10
10
  import os from 'os';
11
11
  import fs from 'fs/promises';
12
12
  import { Worker } from 'worker_threads';
13
- import { glob } from 'glob';
14
13
  import { registerAbortController, deregisterAbortController } from './abortcontroller.js';
14
+ import findPathsToPackages from './findpathstopackages.js';
15
15
 
16
16
  const WORKER_SCRIPT = new URL( './parallelworker.js', import.meta.url );
17
17
 
@@ -49,15 +49,9 @@ export default async function executeInParallel( options ) {
49
49
  } = options;
50
50
 
51
51
  const concurrencyAsInteger = Math.floor( concurrency ) || 1;
52
- const normalizedCwd = upath.toUnix( cwd );
53
- const packages = ( await glob( `${ packagesDirectory }/*/`, {
54
- cwd: normalizedCwd,
55
- absolute: true
56
- } ) ).map( upath.normalize );
57
-
58
- const packagesToProcess = packagesDirectoryFilter ?
59
- packages.filter( packagesDirectoryFilter ) :
60
- packages;
52
+ const packagesToProcess = await findPathsToPackages( cwd, packagesDirectory, {
53
+ packagesDirectoryFilter
54
+ } );
61
55
 
62
56
  const packagesInThreads = getPackagesGroupedByThreads( packagesToProcess, concurrencyAsInteger );
63
57
 
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+
6
+ import upath from 'upath';
7
+ import { glob } from 'glob';
8
+
9
+ /**
10
+ * Returns an array containing paths to packages found in the `packagesDirectory` directory.
11
+ *
12
+ * @param {string} cwd
13
+ * @param {string|null} packagesDirectory
14
+ * @param {object} [options={}]
15
+ * @param {boolean} [options.includePackageJson=false]
16
+ * @param {boolean} [options.includeCwd=false]
17
+ * @param {PackagesDirectoryFilter|null} [options.packagesDirectoryFilter=null]
18
+ * @returns {Array.<string>}
19
+ */
20
+ export default async function findPathsToPackages( cwd, packagesDirectory, options = {} ) {
21
+ const {
22
+ includePackageJson = false,
23
+ includeCwd = false,
24
+ packagesDirectoryFilter = null
25
+ } = options;
26
+
27
+ const packagePaths = await getPackages( cwd, packagesDirectory, includePackageJson );
28
+
29
+ if ( includeCwd ) {
30
+ if ( includePackageJson ) {
31
+ packagePaths.push( upath.join( cwd, 'package.json' ) );
32
+ } else {
33
+ packagePaths.push( cwd );
34
+ }
35
+ }
36
+
37
+ const normalizedPaths = packagePaths.map( item => upath.normalize( item ) );
38
+
39
+ if ( packagesDirectoryFilter ) {
40
+ return normalizedPaths.filter( item => packagesDirectoryFilter( item ) );
41
+ }
42
+
43
+ return normalizedPaths;
44
+ }
45
+
46
+ /**
47
+ * @param {string} cwd
48
+ * @param {string|null} packagesDirectory
49
+ * @param {boolean} includePackageJson
50
+ * @returns {Promise.<Array.<string>>}
51
+ */
52
+ function getPackages( cwd, packagesDirectory, includePackageJson ) {
53
+ if ( !packagesDirectory ) {
54
+ return [];
55
+ }
56
+
57
+ const globOptions = {
58
+ cwd: upath.join( cwd, packagesDirectory ),
59
+ absolute: true
60
+ };
61
+
62
+ let pattern = '*/';
63
+
64
+ if ( includePackageJson ) {
65
+ pattern += 'package.json';
66
+ globOptions.nodir = true;
67
+ }
68
+
69
+ return glob( pattern, globOptions );
70
+ }
71
+
72
+ /**
73
+ * @callback PackagesDirectoryFilter
74
+ *
75
+ * @param {string} packageJsonPath An absolute path to a `package.json` file.
76
+ *
77
+ * @returns {boolean} Whether to include (`true`) or skip (`false`) processing the given directory/package.
78
+ */
@@ -6,11 +6,20 @@
6
6
  import semver from 'semver';
7
7
 
8
8
  /**
9
+ * Returns a version tag from specified `version`.
10
+ *
11
+ * For the official release, returns the "latest" tag. For a non-official release (pre-release),
12
+ * returns the version tag extracted from the passed version.
13
+ *
9
14
  * @param {string} version
10
15
  * @returns {string}
11
16
  */
12
17
  export default function getNpmTagFromVersion( version ) {
13
18
  const [ versionTag ] = semver.prerelease( version ) || [ 'latest' ];
14
19
 
20
+ if ( versionTag.startsWith( 'nightly' ) ) {
21
+ return 'nightly';
22
+ }
23
+
15
24
  return versionTag;
16
25
  }
@@ -3,9 +3,8 @@
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
5
 
6
- import { tools } from '@ckeditor/ckeditor5-dev-utils';
7
6
  import semver from 'semver';
8
- import shellEscape from 'shell-escape';
7
+ import pacote from 'pacote';
9
8
 
10
9
  /**
11
10
  * This util aims to verify if the given `packageName` can be published with the given `version` on the `npmTag`.
@@ -16,10 +15,9 @@ import shellEscape from 'shell-escape';
16
15
  * @returns {Promise.<boolean>}
17
16
  */
18
17
  export default async function isVersionPublishableForTag( packageName, version, npmTag ) {
19
- const command = `npm view ${ shellEscape( [ packageName ] ) }@${ shellEscape( [ npmTag ] ) } version --silent`;
20
- const npmVersion = await tools.shExec( command, { async: true, verbosity: 'silent' } )
21
- .then( value => value.trim() )
22
- // An `npmTag` does not exist.
18
+ const npmVersion = await pacote.manifest( `${ packageName }@${ npmTag }` )
19
+ .then( ( { version } ) => version )
20
+ // An `npmTag` does not exist, or it's a first release of a package.
23
21
  .catch( () => null );
24
22
 
25
23
  if ( npmVersion && semver.lte( version, npmVersion ) ) {
@@ -7,23 +7,29 @@ import inquirer from 'inquirer';
7
7
  import semver from 'semver';
8
8
  import chalk from 'chalk';
9
9
  import { CLI_INDENT_SIZE } from './constants.js';
10
+ import checkVersionAvailability from './checkversionavailability.js';
10
11
 
11
12
  /**
12
13
  * Asks a user for providing the new version for a major release.
13
14
  *
14
- * @param {string} version
15
- * @param {string} foundPackage
16
- * @param {string} bumpType
17
- * @param {object} [options={}]
15
+ * @param {object} options
16
+ * @param {string} options.packageName
17
+ * @param {string} options.version
18
+ * @param {string} options.bumpType
18
19
  * @param {number} [options.indentLevel=0] The indent level.
19
20
  * @returns {Promise.<string>}
20
21
  */
21
- export default async function provideNewVersionForMonoRepository( version, foundPackage, bumpType, options = {} ) {
22
- const indentLevel = options.indentLevel || 0;
23
- const suggestedVersion = semver.inc( version, bumpType );
22
+ export default async function provideNewVersionForMonoRepository( options ) {
23
+ const {
24
+ version,
25
+ packageName,
26
+ bumpType,
27
+ indentLevel = 0
28
+ } = options;
24
29
 
30
+ const suggestedVersion = semver.inc( version, bumpType );
25
31
  const message = 'Type the new version ' +
26
- `(current highest: "${ version }" found in "${ chalk.underline( foundPackage ) }", suggested: "${ suggestedVersion }"):`;
32
+ `(current highest: "${ version }" found in "${ chalk.underline( packageName ) }", suggested: "${ suggestedVersion }"):`;
27
33
 
28
34
  const versionQuestion = {
29
35
  type: 'input',
@@ -35,12 +41,22 @@ export default async function provideNewVersionForMonoRepository( version, found
35
41
  return input.trim();
36
42
  },
37
43
 
38
- validate( input ) {
44
+ async validate( input ) {
39
45
  if ( !semver.valid( input ) ) {
40
46
  return 'Please provide a valid version.';
41
47
  }
42
48
 
43
- return semver.gt( input, version ) ? true : `Provided version must be higher than "${ version }".`;
49
+ if ( !semver.gt( input, version ) ) {
50
+ return `Provided version must be higher than "${ version }".`;
51
+ }
52
+
53
+ const isAvailable = await checkVersionAvailability( input, packageName );
54
+
55
+ if ( !isAvailable ) {
56
+ return 'Given version is already taken.';
57
+ }
58
+
59
+ return true;
44
60
  },
45
61
  prefix: ' '.repeat( indentLevel * CLI_INDENT_SIZE ) + chalk.cyan( '?' )
46
62
  };
@@ -7,22 +7,30 @@ import inquirer from 'inquirer';
7
7
  import semver from 'semver';
8
8
  import chalk from 'chalk';
9
9
  import { CLI_INDENT_SIZE } from './constants.js';
10
+ import checkVersionAvailability from './checkversionavailability.js';
10
11
 
11
12
  /**
12
13
  * Asks a user for providing the new version.
13
14
  *
14
- * @param {string} packageVersion
15
- * @param {string|null} releaseTypeOrNewVersion
16
- * @param {object} [options]
15
+ * @param {object} options
16
+ * @param {string} options.packageName
17
+ * @param {string} options.version
18
+ * @param {string|null} options.releaseTypeOrNewVersion
17
19
  * @param {boolean} [options.disableInternalVersion=false] Whether to "internal" version is enabled.
18
20
  * @param {boolean} [options.disableSkipVersion=false] Whether to "skip" version is enabled.
19
21
  * @param {number} [options.indentLevel=0] The indent level.
20
22
  * @returns {Promise.<string>}
21
23
  */
22
- export default function provideVersion( packageVersion, releaseTypeOrNewVersion, options = {} ) {
23
- const indentLevel = options.indentLevel || 0;
24
+ export default function provideVersion( options ) {
25
+ const {
26
+ packageName,
27
+ version,
28
+ releaseTypeOrNewVersion,
29
+ indentLevel = 0
30
+ } = options;
31
+
24
32
  const suggestedVersion = getSuggestedVersion( {
25
- packageVersion,
33
+ version,
26
34
  releaseTypeOrNewVersion,
27
35
  disableInternalVersion: options.disableInternalVersion
28
36
  } );
@@ -33,7 +41,7 @@ export default function provideVersion( packageVersion, releaseTypeOrNewVersion,
33
41
  message = 'Type the new version or "skip"';
34
42
  }
35
43
 
36
- message += ` (suggested: "${ suggestedVersion }", current: "${ packageVersion }"):`;
44
+ message += ` (suggested: "${ suggestedVersion }", current: "${ version }"):`;
37
45
 
38
46
  const versionQuestion = {
39
47
  type: 'input',
@@ -45,7 +53,7 @@ export default function provideVersion( packageVersion, releaseTypeOrNewVersion,
45
53
  return input.trim();
46
54
  },
47
55
 
48
- validate( input ) {
56
+ async validate( input ) {
49
57
  if ( !options.disableSkipVersion && input === 'skip' ) {
50
58
  return true;
51
59
  }
@@ -54,8 +62,17 @@ export default function provideVersion( packageVersion, releaseTypeOrNewVersion,
54
62
  return true;
55
63
  }
56
64
 
57
- // TODO: Check whether provided version is available.
58
- return semver.valid( input ) ? true : 'Please provide a valid version.';
65
+ if ( !semver.valid( input ) ) {
66
+ return 'Please provide a valid version.';
67
+ }
68
+
69
+ const isAvailable = await checkVersionAvailability( input, packageName );
70
+
71
+ if ( !isAvailable ) {
72
+ return 'Given version is already taken.';
73
+ }
74
+
75
+ return true;
59
76
  },
60
77
 
61
78
  prefix: ' '.repeat( indentLevel * CLI_INDENT_SIZE ) + chalk.cyan( '?' )
@@ -67,12 +84,12 @@ export default function provideVersion( packageVersion, releaseTypeOrNewVersion,
67
84
 
68
85
  /**
69
86
  * @param {object} options
70
- * @param {string} options.packageVersion
87
+ * @param {string} options.version
71
88
  * @param {string|null} options.releaseTypeOrNewVersion
72
89
  * @param {boolean} options.disableInternalVersion
73
90
  * @returns {string}
74
91
  */
75
- function getSuggestedVersion( { packageVersion, releaseTypeOrNewVersion, disableInternalVersion } ) {
92
+ function getSuggestedVersion( { version, releaseTypeOrNewVersion, disableInternalVersion } ) {
76
93
  if ( !releaseTypeOrNewVersion || releaseTypeOrNewVersion === 'skip' ) {
77
94
  return 'skip';
78
95
  }
@@ -85,14 +102,14 @@ function getSuggestedVersion( { packageVersion, releaseTypeOrNewVersion, disable
85
102
  return disableInternalVersion ? 'skip' : 'internal';
86
103
  }
87
104
 
88
- if ( semver.prerelease( packageVersion ) ) {
105
+ if ( semver.prerelease( version ) ) {
89
106
  releaseTypeOrNewVersion = 'prerelease';
90
107
  }
91
108
 
92
109
  // If package's version is below the '1.0.0', bump the 'minor' instead of 'major'
93
- if ( releaseTypeOrNewVersion === 'major' && semver.gt( '1.0.0', packageVersion ) ) {
94
- return semver.inc( packageVersion, 'minor' );
110
+ if ( releaseTypeOrNewVersion === 'major' && semver.gt( '1.0.0', version ) ) {
111
+ return semver.inc( version, 'minor' );
95
112
  }
96
113
 
97
- return semver.inc( packageVersion, releaseTypeOrNewVersion );
114
+ return semver.inc( version, releaseTypeOrNewVersion );
98
115
  }
@@ -14,26 +14,16 @@
14
14
  export default async function publishPackageOnNpmCallback( packagePath, taskOptions ) {
15
15
  const { tools } = await import( '@ckeditor/ckeditor5-dev-utils' );
16
16
  const { default: fs } = await import( 'fs-extra' );
17
- const { default: path } = await import( 'upath' );
18
17
 
19
- const options = {
20
- cwd: packagePath,
21
- async: true,
22
- verbosity: 'error'
23
- };
24
-
25
- const result = await tools.shExec( `npm publish --access=public --tag ${ taskOptions.npmTag }`, options )
26
- .catch( e => {
27
- const packageName = path.basename( packagePath );
28
-
29
- if ( e.toString().includes( 'code E409' ) ) {
30
- return { shouldKeepDirectory: true };
31
- }
32
-
33
- throw new Error( `Unable to publish "${ packageName }" package.` );
18
+ try {
19
+ await tools.shExec( `npm publish --access=public --tag ${ taskOptions.npmTag }`, {
20
+ cwd: packagePath,
21
+ async: true,
22
+ verbosity: 'error'
34
23
  } );
35
24
 
36
- if ( !result || !result.shouldKeepDirectory ) {
37
25
  await fs.remove( packagePath );
26
+ } catch {
27
+ // Do nothing if an error occurs. A parent task will handle it.
38
28
  }
39
29
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { tools } from '@ckeditor/ckeditor5-dev-utils';
7
+ import pacote from 'pacote';
7
8
  import getChangelog from './getchangelog.js';
8
9
  import getPackageJson from './getpackagejson.js';
9
10
 
@@ -37,9 +38,9 @@ export function getLastFromChangelog( cwd = process.cwd() ) {
37
38
  export function getLastPreRelease( releaseIdentifier, cwd = process.cwd() ) {
38
39
  const packageName = getPackageJson( cwd ).name;
39
40
 
40
- return tools.shExec( `npm view ${ packageName } versions --json`, { verbosity: 'silent', async: true } )
41
+ return pacote.packument( packageName )
41
42
  .then( result => {
42
- const lastVersion = JSON.parse( result )
43
+ const lastVersion = Object.keys( result.versions )
43
44
  .filter( version => version.startsWith( releaseIdentifier ) )
44
45
  .sort( ( a, b ) => a.localeCompare( b, undefined, { numeric: true } ) )
45
46
  .pop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-dev-release-tools",
3
- "version": "44.0.0",
3
+ "version": "44.1.0",
4
4
  "description": "Tools used for releasing CKEditor 5 and related packages.",
5
5
  "keywords": [],
6
6
  "author": "CKSource (http://cksource.com/)",
@@ -22,7 +22,7 @@
22
22
  "lib"
23
23
  ],
24
24
  "dependencies": {
25
- "@ckeditor/ckeditor5-dev-utils": "^44.0.0",
25
+ "@ckeditor/ckeditor5-dev-utils": "^44.1.0",
26
26
  "@octokit/rest": "^21.0.0",
27
27
  "chalk": "^5.0.0",
28
28
  "cli-columns": "^4.0.0",
@@ -38,6 +38,7 @@
38
38
  "inquirer": "^11.0.0",
39
39
  "lodash-es": "^4.17.21",
40
40
  "minimatch": "^9.0.0",
41
+ "pacote": "^19.0.0",
41
42
  "semver": "^7.6.3",
42
43
  "shell-escape": "^0.2.0",
43
44
  "upath": "^2.0.1"