@ckeditor/ckeditor5-dev-release-tools 53.4.0 → 54.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/tasks/cleanuppackages.js +46 -14
- package/lib/tasks/preparerepository.js +7 -7
- package/lib/tasks/publishpackages.js +4 -3
- package/lib/tasks/reassignnpmtags.js +6 -7
- package/lib/tasks/updatedependencies.js +4 -3
- package/lib/tasks/updateversions.js +4 -3
- package/lib/utils/assertfilestopublish.js +4 -3
- package/lib/utils/assertnpmtag.js +4 -3
- package/lib/utils/assertpackages.js +4 -3
- package/lib/utils/confirmnpmtag.js +2 -2
- package/lib/utils/publishpackageonnpmcallback.js +2 -2
- package/package.json +4 -6
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* For licensing, see LICENSE.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import fs from 'fs
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
7
|
import upath from 'upath';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
9
|
import { workspaces } from '@ckeditor/ckeditor5-dev-utils';
|
|
@@ -18,7 +18,7 @@ import { workspaces } from '@ckeditor/ckeditor5-dev-utils';
|
|
|
18
18
|
* - `README.md`
|
|
19
19
|
* - file pointed by the `main` field from `package.json`
|
|
20
20
|
* - file pointed by the `types` field from `package.json`
|
|
21
|
-
* - Removes unnecessary fields from the `package.json` file.
|
|
21
|
+
* - Removes unnecessary fields from the `package.json` file. Supports nested field paths, e.g. `engines.pnpm`.
|
|
22
22
|
*
|
|
23
23
|
* @param {object} options
|
|
24
24
|
* @param {string} options.packagesDirectory Relative path to a location of packages to be cleaned up.
|
|
@@ -34,12 +34,13 @@ export default async function cleanUpPackages( options ) {
|
|
|
34
34
|
|
|
35
35
|
for ( const packageJsonPath of packageJsonPaths ) {
|
|
36
36
|
const packagePath = upath.dirname( packageJsonPath );
|
|
37
|
-
const
|
|
37
|
+
const packageJsonFile = await fs.readFile( packageJsonPath, 'utf-8' );
|
|
38
|
+
const packageJson = JSON.parse( packageJsonFile );
|
|
38
39
|
|
|
39
40
|
await cleanUpPackageDirectory( packageJson, packagePath );
|
|
40
41
|
cleanUpPackageJson( packageJson, packageJsonFieldsToRemove, preservePostInstallHook );
|
|
41
42
|
|
|
42
|
-
await fs.
|
|
43
|
+
await fs.writeFile( packageJsonPath, JSON.stringify( packageJson, null, 2 ) );
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -96,7 +97,7 @@ async function cleanUpPackageDirectory( packageJson, packagePath ) {
|
|
|
96
97
|
} );
|
|
97
98
|
|
|
98
99
|
for ( const file of files ) {
|
|
99
|
-
await fs.
|
|
100
|
+
await fs.rm( file );
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -114,12 +115,12 @@ async function cleanUpPackageDirectory( packageJson, packagePath ) {
|
|
|
114
115
|
const isEmpty = ( await fs.readdir( directory ) ).length === 0;
|
|
115
116
|
|
|
116
117
|
if ( isEmpty ) {
|
|
117
|
-
await fs.
|
|
118
|
+
await fs.rm( directory, { recursive: true, force: true } );
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// Remove `node_modules`.
|
|
122
|
-
await fs.
|
|
123
|
+
await fs.rm( upath.join( packagePath, 'node_modules' ), { recursive: true, force: true } );
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
/**
|
|
@@ -156,16 +157,14 @@ function getIgnoredFilePatterns( packageJson ) {
|
|
|
156
157
|
* @param {boolean} preservePostInstallHook
|
|
157
158
|
*/
|
|
158
159
|
function cleanUpPackageJson( packageJson, packageJsonFieldsToRemove, preservePostInstallHook ) {
|
|
159
|
-
for ( const
|
|
160
|
-
if (
|
|
160
|
+
for ( const field of packageJsonFieldsToRemove ) {
|
|
161
|
+
if ( field === 'scripts' && preservePostInstallHook && packageJson.scripts?.postinstall ) {
|
|
162
|
+
packageJson.scripts = { postinstall: packageJson.scripts.postinstall };
|
|
163
|
+
|
|
161
164
|
continue;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
|
|
165
|
-
packageJson.scripts = { 'postinstall': packageJson.scripts.postinstall };
|
|
166
|
-
} else {
|
|
167
|
-
delete packageJson[ key ];
|
|
168
|
-
}
|
|
167
|
+
removeField( packageJson, field );
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
170
|
|
|
@@ -183,6 +182,39 @@ function sortPathsFromDeepestFirst( firstPath, secondPath ) {
|
|
|
183
182
|
return secondPathSegments - firstPathSegments;
|
|
184
183
|
}
|
|
185
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Removes provided field from an object. Supports nested field paths, e.g. "a.b.c". It modifies the source `obj` argument.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} obj Source object containing a field to remove.
|
|
189
|
+
* @param {string} path Path to the field to be removed.
|
|
190
|
+
*/
|
|
191
|
+
function removeField( obj, path ) {
|
|
192
|
+
const parts = path.split( '.' );
|
|
193
|
+
const lastPart = parts.pop();
|
|
194
|
+
|
|
195
|
+
let current = obj;
|
|
196
|
+
|
|
197
|
+
for ( const part of parts ) {
|
|
198
|
+
current = current[ part ];
|
|
199
|
+
|
|
200
|
+
if ( !isObject( current ) ) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
delete current[ lastPart ];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Checks if provided value is an object literal.
|
|
210
|
+
*
|
|
211
|
+
* @param {*} value Value to check.
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function isObject( value ) {
|
|
215
|
+
return value !== null && typeof value === 'object' && !Array.isArray( value );
|
|
216
|
+
}
|
|
217
|
+
|
|
186
218
|
/**
|
|
187
219
|
* @typedef {['devDependencies','depcheckIgnore','scripts','private']} DefaultFieldsToRemove
|
|
188
220
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* For licensing, see LICENSE.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import fs from 'fs
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
7
|
import { glob } from 'glob';
|
|
8
8
|
import upath from 'upath';
|
|
9
9
|
|
|
@@ -35,7 +35,7 @@ export default async function prepareRepository( options ) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const outputDirectoryPath = upath.join( cwd, outputDirectory );
|
|
38
|
-
await fs.
|
|
38
|
+
await fs.mkdir( outputDirectoryPath, { recursive: true } );
|
|
39
39
|
const outputDirContent = await fs.readdir( outputDirectoryPath );
|
|
40
40
|
|
|
41
41
|
if ( outputDirContent.length ) {
|
|
@@ -97,15 +97,15 @@ async function processRootPackage( { cwd, rootPackageJson, outputDirectoryPath }
|
|
|
97
97
|
const rootPackageOutputPath = upath.join( outputDirectoryPath, rootPackageDirName );
|
|
98
98
|
const pkgJsonOutputPath = upath.join( rootPackageOutputPath, 'package.json' );
|
|
99
99
|
|
|
100
|
-
await fs.
|
|
101
|
-
await fs.
|
|
100
|
+
await fs.mkdir( rootPackageOutputPath, { recursive: true } );
|
|
101
|
+
await fs.writeFile( pkgJsonOutputPath, JSON.stringify( rootPackageJson, null, 2 ) + '\n' );
|
|
102
102
|
|
|
103
103
|
return ( await glob( rootPackageJson.files ) )
|
|
104
104
|
.map( absoluteFilePath => {
|
|
105
105
|
const relativeFilePath = upath.relative( cwd, absoluteFilePath );
|
|
106
106
|
const absoluteFileOutputPath = upath.join( rootPackageOutputPath, relativeFilePath );
|
|
107
107
|
|
|
108
|
-
return fs.
|
|
108
|
+
return fs.cp( absoluteFilePath, absoluteFileOutputPath, { recursive: true } );
|
|
109
109
|
} );
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -130,13 +130,13 @@ async function processMonorepoPackages( { cwd, packagesDirectory, packagesToCopy
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
const pkgJsonPath = upath.join( packagePath, 'package.json' );
|
|
133
|
-
const hasPkgJson = await fs.
|
|
133
|
+
const hasPkgJson = await fs.access( pkgJsonPath ).then( () => true, () => false );
|
|
134
134
|
|
|
135
135
|
if ( !hasPkgJson ) {
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
return fs.
|
|
139
|
+
return fs.cp( packagePath, upath.join( outputDirectoryPath, packageDir ), { recursive: true } );
|
|
140
140
|
} );
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import upath from 'upath';
|
|
7
|
-
import fs from 'fs
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
8
|
import assertNpmAuthorization from '../utils/assertnpmauthorization.js';
|
|
9
9
|
import assertPackages from '../utils/assertpackages.js';
|
|
10
10
|
import assertNpmTag from '../utils/assertnpmtag.js';
|
|
@@ -130,11 +130,12 @@ export default async function publishPackages( options ) {
|
|
|
130
130
|
|
|
131
131
|
async function removeAlreadyPublishedPackages( packagePaths ) {
|
|
132
132
|
for ( const absolutePackagePath of packagePaths ) {
|
|
133
|
-
const
|
|
133
|
+
const pkgJsonFile = await fs.readFile( upath.join( absolutePackagePath, 'package.json' ), 'utf-8' );
|
|
134
|
+
const pkgJson = JSON.parse( pkgJsonFile );
|
|
134
135
|
const isAvailable = await npm.checkVersionAvailability( pkgJson.version, pkgJson.name );
|
|
135
136
|
|
|
136
137
|
if ( !isAvailable ) {
|
|
137
|
-
await fs.
|
|
138
|
+
await fs.rm( absolutePackagePath, { recursive: true, force: true } );
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
}
|
|
@@ -5,15 +5,14 @@
|
|
|
5
5
|
* For licensing, see LICENSE.md.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
import columns from 'cli-columns';
|
|
10
8
|
import { tools } from '@ckeditor/ckeditor5-dev-utils';
|
|
11
|
-
import
|
|
9
|
+
import { styleText, promisify } from 'util';
|
|
10
|
+
import columns from 'cli-columns';
|
|
12
11
|
import shellEscape from 'shell-escape';
|
|
13
12
|
import assertNpmAuthorization from '../utils/assertnpmauthorization.js';
|
|
14
13
|
import { exec } from 'child_process';
|
|
15
14
|
|
|
16
|
-
const execPromise =
|
|
15
|
+
const execPromise = promisify( exec );
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Used to switch the tags from `staging` to `latest` for specified array of packages.
|
|
@@ -71,17 +70,17 @@ export default async function reassignNpmTags( options ) {
|
|
|
71
70
|
counter.finish();
|
|
72
71
|
|
|
73
72
|
if ( packagesUpdated.length ) {
|
|
74
|
-
console.log(
|
|
73
|
+
console.log( styleText( [ 'green', 'bold' ], '✨ Tags updated:' ) );
|
|
75
74
|
console.log( columns( packagesUpdated ) );
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
if ( packagesSkipped.length ) {
|
|
79
|
-
console.log(
|
|
78
|
+
console.log( styleText( [ 'yellow', 'bold' ], '⬇️ Packages skipped:' ) );
|
|
80
79
|
console.log( columns( packagesSkipped ) );
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
if ( errors.length ) {
|
|
84
|
-
console.log(
|
|
83
|
+
console.log( styleText( [ 'red', 'bold' ], '🐛 Errors found:' ) );
|
|
85
84
|
errors.forEach( msg => console.log( `* ${ msg }` ) );
|
|
86
85
|
}
|
|
87
86
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { workspaces } from '@ckeditor/ckeditor5-dev-utils';
|
|
7
|
-
import fs from 'fs
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
8
|
import upath from 'upath';
|
|
9
9
|
|
|
10
10
|
const { normalizeTrim } = upath;
|
|
@@ -51,13 +51,14 @@ export default async function updateDependencies( options ) {
|
|
|
51
51
|
);
|
|
52
52
|
|
|
53
53
|
for ( const pkgJsonPath of pkgJsonPaths ) {
|
|
54
|
-
const
|
|
54
|
+
const pkgJsonFile = await fs.readFile( pkgJsonPath, 'utf-8' );
|
|
55
|
+
const pkgJson = JSON.parse( pkgJsonFile );
|
|
55
56
|
|
|
56
57
|
updateVersion( version, shouldUpdateVersionCallback, pkgJson.dependencies );
|
|
57
58
|
updateVersion( version, shouldUpdateVersionCallback, pkgJson.devDependencies );
|
|
58
59
|
updateVersion( version, shouldUpdateVersionCallback, pkgJson.peerDependencies );
|
|
59
60
|
|
|
60
|
-
await fs.
|
|
61
|
+
await fs.writeFile( pkgJsonPath, JSON.stringify( pkgJson, null, 2 ) );
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* For licensing, see LICENSE.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import fs from 'fs/promises';
|
|
6
7
|
import { workspaces } from '@ckeditor/ckeditor5-dev-utils';
|
|
7
8
|
import upath from 'upath';
|
|
8
|
-
import fs from 'fs-extra';
|
|
9
9
|
import semver from 'semver';
|
|
10
10
|
|
|
11
11
|
const { normalizeTrim } = upath;
|
|
@@ -51,10 +51,11 @@ export default async function updateVersions( options ) {
|
|
|
51
51
|
checkIfVersionIsValid( version );
|
|
52
52
|
|
|
53
53
|
for ( const pkgJsonPath of pkgJsonPaths ) {
|
|
54
|
-
const
|
|
54
|
+
const pkgJsonFile = await fs.readFile( pkgJsonPath, 'utf-8' );
|
|
55
|
+
const pkgJson = JSON.parse( pkgJsonFile );
|
|
55
56
|
|
|
56
57
|
pkgJson.version = version;
|
|
57
|
-
await fs.
|
|
58
|
+
await fs.writeFile( pkgJsonPath, JSON.stringify( pkgJson, null, 2 ) );
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* For licensing, see LICENSE.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import fs from 'fs
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
7
|
import upath from 'upath';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
9
|
|
|
@@ -19,8 +19,9 @@ export default async function assertFilesToPublish( packagePaths, optionalEntrie
|
|
|
19
19
|
|
|
20
20
|
for ( const packagePath of packagePaths ) {
|
|
21
21
|
const requiredEntries = [];
|
|
22
|
-
const
|
|
23
|
-
const
|
|
22
|
+
const path = upath.join( packagePath, 'package.json' );
|
|
23
|
+
const file = await fs.readFile( path, 'utf-8' );
|
|
24
|
+
const packageJson = JSON.parse( file );
|
|
24
25
|
|
|
25
26
|
if ( packageJson.main ) {
|
|
26
27
|
requiredEntries.push( packageJson.main );
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* For licensing, see LICENSE.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import fs from 'fs
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
7
|
import upath from 'upath';
|
|
8
8
|
import getNpmTagFromVersion from './getnpmtagfromversion.js';
|
|
9
9
|
|
|
@@ -23,8 +23,9 @@ export default async function assertNpmTag( packagePaths, npmTag ) {
|
|
|
23
23
|
const errors = [];
|
|
24
24
|
|
|
25
25
|
for ( const packagePath of packagePaths ) {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
26
|
+
const path = upath.join( packagePath, 'package.json' );
|
|
27
|
+
const file = await fs.readFile( path, 'utf-8' );
|
|
28
|
+
const packageJson = JSON.parse( file );
|
|
28
29
|
const versionTag = getNpmTagFromVersion( packageJson.version );
|
|
29
30
|
|
|
30
31
|
if ( versionTag === npmTag ) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* For licensing, see LICENSE.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import fs from 'fs
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
7
|
import upath from 'upath';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -24,13 +24,14 @@ export default async function assertPackages( packagePaths, options ) {
|
|
|
24
24
|
for ( const packagePath of packagePaths ) {
|
|
25
25
|
const packageName = upath.basename( packagePath );
|
|
26
26
|
const packageJsonPath = upath.join( packagePath, 'package.json' );
|
|
27
|
+
const hasPkgJson = await fs.access( packageJsonPath ).then( () => true, () => false );
|
|
27
28
|
|
|
28
|
-
if (
|
|
29
|
+
if ( hasPkgJson ) {
|
|
29
30
|
if ( !requireEntryPoint ) {
|
|
30
31
|
continue;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const { name: packageName, main: entryPoint } = await fs.
|
|
34
|
+
const { name: packageName, main: entryPoint } = JSON.parse( await fs.readFile( packageJsonPath, 'utf-8' ) );
|
|
34
35
|
|
|
35
36
|
if ( optionalEntryPointPackages.includes( packageName ) ) {
|
|
36
37
|
continue;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* For licensing, see LICENSE.md.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { styleText } from 'util';
|
|
6
7
|
import inquirer from 'inquirer';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Asks a user for a confirmation for updating and tagging versions of the packages.
|
|
@@ -15,7 +15,7 @@ import chalk from 'chalk';
|
|
|
15
15
|
*/
|
|
16
16
|
export default function confirmNpmTag( versionTag, npmTag ) {
|
|
17
17
|
const areVersionsEqual = versionTag === npmTag;
|
|
18
|
-
const color = areVersionsEqual ?
|
|
18
|
+
const color = text => areVersionsEqual ? styleText( 'magenta', text ) : styleText( 'red', text );
|
|
19
19
|
|
|
20
20
|
// eslint-disable-next-line @stylistic/max-len
|
|
21
21
|
const message = `The next release bumps the "${ color( versionTag ) }" version. Should it be published to npm as "${ color( npmTag ) }"?`;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export default async function publishPackageOnNpmCallback( packagePath, taskOptions ) {
|
|
15
15
|
const { tools } = await import( '@ckeditor/ckeditor5-dev-utils' );
|
|
16
|
-
const {
|
|
16
|
+
const { rm } = await import( 'fs/promises' );
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
19
|
await tools.shExec( `npm publish --access=public --tag ${ taskOptions.npmTag }`, {
|
|
@@ -22,7 +22,7 @@ export default async function publishPackageOnNpmCallback( packagePath, taskOpti
|
|
|
22
22
|
verbosity: 'silent'
|
|
23
23
|
} );
|
|
24
24
|
|
|
25
|
-
await
|
|
25
|
+
await rm( packagePath, { recursive: true, force: true } );
|
|
26
26
|
} catch {
|
|
27
27
|
// Do nothing if an error occurs. A parent task will handle it.
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckeditor/ckeditor5-dev-release-tools",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "54.1.0",
|
|
4
4
|
"description": "Tools used for releasing CKEditor 5 and related packages.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"author": "CKSource (http://cksource.com/)",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"directory": "packages/ckeditor5-dev-release-tools"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
16
|
+
"node": ">=24.11.0",
|
|
17
17
|
"npm": ">=5.7.1"
|
|
18
18
|
},
|
|
19
19
|
"type": "module",
|
|
@@ -22,11 +22,9 @@
|
|
|
22
22
|
"lib"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ckeditor/ckeditor5-dev-utils": "^
|
|
25
|
+
"@ckeditor/ckeditor5-dev-utils": "^54.1.0",
|
|
26
26
|
"@octokit/rest": "^22.0.0",
|
|
27
|
-
"chalk": "^5.0.0",
|
|
28
27
|
"cli-columns": "^4.0.0",
|
|
29
|
-
"fs-extra": "^11.0.0",
|
|
30
28
|
"glob": "^11.0.2",
|
|
31
29
|
"inquirer": "^12.5.2",
|
|
32
30
|
"semver": "^7.6.3",
|
|
@@ -34,4 +32,4 @@
|
|
|
34
32
|
"simple-git": "^3.27.0",
|
|
35
33
|
"upath": "^2.0.1"
|
|
36
34
|
}
|
|
37
|
-
}
|
|
35
|
+
}
|