super-release 0.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/README.md +308 -0
- package/npm/bin/super-release.js +20 -0
- package/npm/install.js +69 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# super-release
|
|
2
|
+
|
|
3
|
+
A fast [semantic-release](https://semantic-release.gitbook.io/semantic-release) alternative for monorepos, written in Rust.
|
|
4
|
+
|
|
5
|
+
Analyzes [conventional commits](https://www.conventionalcommits.org/) to determine version bumps, generate changelogs, update `package.json` files, publish to npm, and create git tags -- across all packages in a monorepo, in parallel.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Monorepo-first: discovers all `package.json` packages and associates commits by changed files
|
|
10
|
+
- Parallel commit analysis using rayon (configurable with `-j`)
|
|
11
|
+
- Prerelease branches (`beta`, `next`, or dynamic from branch name)
|
|
12
|
+
- Maintenance branches (`1.x`, `2.x`) with major-version capping
|
|
13
|
+
- Changelog generation powered by [git-cliff](https://git-cliff.org/)
|
|
14
|
+
- Plugin system: changelog, npm, git-tag (extensible)
|
|
15
|
+
- Configurable tag format templates
|
|
16
|
+
- Dependency-aware npm publish (topological order)
|
|
17
|
+
- Dry-run mode with pretty, truncated output
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cargo install --path .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or build a release binary:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cargo build --release
|
|
29
|
+
# Binary at target/release/super-release
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Preview what would be released
|
|
36
|
+
super-release --dry-run
|
|
37
|
+
|
|
38
|
+
# Run a release
|
|
39
|
+
super-release
|
|
40
|
+
|
|
41
|
+
# Use 4 threads for commit analysis
|
|
42
|
+
super-release -j 4
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## CLI Reference
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Usage: super-release [OPTIONS]
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
-n, --dry-run Show what would happen without making changes
|
|
52
|
+
-C, --path <PATH> Repository root [default: .]
|
|
53
|
+
-c, --config <CONFIG> Path to config file [default: .release.yaml]
|
|
54
|
+
-v, --verbose Verbose output
|
|
55
|
+
-j, --jobs <JOBS> Parallel jobs for commit analysis [default: 50% of CPUs]
|
|
56
|
+
-h, --help Print help
|
|
57
|
+
-V, --version Print version
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How It Works
|
|
61
|
+
|
|
62
|
+
1. **Discover packages** -- finds all directories with a `package.json`
|
|
63
|
+
2. **Resolve tags** -- finds the latest release tag per package (filtered by branch context)
|
|
64
|
+
3. **Walk commits** -- only analyzes commits since the oldest tag (not the entire history)
|
|
65
|
+
4. **Associate commits to packages** -- maps changed files to their owning package
|
|
66
|
+
5. **Calculate versions** -- uses git-cliff's conventional commit analysis to determine bump levels
|
|
67
|
+
6. **Run plugins** -- changelog, npm publish, git tag (in configured order)
|
|
68
|
+
|
|
69
|
+
## Conventional Commits
|
|
70
|
+
|
|
71
|
+
super-release follows the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
|
72
|
+
|
|
73
|
+
| Commit | Bump |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `fix: ...` | patch |
|
|
76
|
+
| `feat: ...` | minor |
|
|
77
|
+
| `feat!: ...` or `BREAKING CHANGE:` in footer | major |
|
|
78
|
+
| `perf: ...` | patch |
|
|
79
|
+
| `chore: ...`, `docs: ...`, `ci: ...` | no release |
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
Create a `.release.yaml` (or `.release.yml`, `.super-release.yaml`) in your repository root. All fields are optional and have sensible defaults.
|
|
84
|
+
|
|
85
|
+
### Full Example
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
# Branch configurations
|
|
89
|
+
branches:
|
|
90
|
+
# Stable branches (simple string = stable, no prerelease)
|
|
91
|
+
- main
|
|
92
|
+
- master
|
|
93
|
+
|
|
94
|
+
# Prerelease with a fixed channel name
|
|
95
|
+
- name: beta
|
|
96
|
+
prerelease: beta # -> 2.0.0-beta.1, 2.0.0-beta.2, ...
|
|
97
|
+
|
|
98
|
+
- name: next
|
|
99
|
+
prerelease: next # -> 2.0.0-next.1, ...
|
|
100
|
+
|
|
101
|
+
# Prerelease using the branch name as the channel (for branch patterns)
|
|
102
|
+
- name: "test-*"
|
|
103
|
+
prerelease: true # branch test-foo -> 2.0.0-test-foo.1, ...
|
|
104
|
+
|
|
105
|
+
# Maintenance branches (caps major version, breaking changes -> minor)
|
|
106
|
+
- name: "1.x"
|
|
107
|
+
maintenance: true # -> 1.5.1, 1.6.0 (never 2.0.0)
|
|
108
|
+
|
|
109
|
+
# Tag format templates (use {version} and {name} placeholders)
|
|
110
|
+
tag_format: "v{version}" # root package: v1.2.3
|
|
111
|
+
tag_format_package: "{name}/v{version}" # sub-packages: @acme/core/v1.2.3
|
|
112
|
+
|
|
113
|
+
# Packages to exclude from releasing (substring match on package name)
|
|
114
|
+
exclude:
|
|
115
|
+
- my-private-pkg
|
|
116
|
+
|
|
117
|
+
# Plugins run in order: prepare phase first, then publish phase
|
|
118
|
+
plugins:
|
|
119
|
+
- name: changelog
|
|
120
|
+
- name: npm
|
|
121
|
+
- name: git-tag
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Reference
|
|
125
|
+
|
|
126
|
+
#### `branches`
|
|
127
|
+
|
|
128
|
+
Defines which branches can produce releases and what kind.
|
|
129
|
+
|
|
130
|
+
| Form | Type | Example versions |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `- main` | Stable | `1.0.0`, `1.1.0`, `2.0.0` |
|
|
133
|
+
| `- name: beta`<br>` prerelease: beta` | Prerelease (fixed channel) | `2.0.0-beta.1`, `2.0.0-beta.2` |
|
|
134
|
+
| `- name: "test-*"`<br>` prerelease: true` | Prerelease (branch name as channel) | `2.0.0-test-my-feature.1` |
|
|
135
|
+
| `- name: "1.x"`<br>` maintenance: true` | Maintenance | `1.5.1`, `1.6.0` (major capped) |
|
|
136
|
+
|
|
137
|
+
**Tag filtering by branch**: Stable branches only see stable tags. Prerelease branches see their own channel's tags plus stable tags. This prevents a `v2.0.0-beta.1` tag from affecting version calculation on `main`.
|
|
138
|
+
|
|
139
|
+
**Prerelease behavior**: If the latest tag for a package is already on the same prerelease channel (e.g. `v2.0.0-beta.3`), the next release increments the prerelease number (`v2.0.0-beta.4`). If coming from a stable version, it computes the next stable bump and appends the channel (`v1.1.0-beta.1`).
|
|
140
|
+
|
|
141
|
+
**Maintenance behavior**: Breaking changes (`feat!:`) are demoted to minor bumps so the major version never increases on a maintenance branch.
|
|
142
|
+
|
|
143
|
+
Default: `["main", "master"]`
|
|
144
|
+
|
|
145
|
+
#### `tag_format`
|
|
146
|
+
|
|
147
|
+
Template for root package tags. Placeholders:
|
|
148
|
+
- `{version}` -- the semver version (e.g. `1.2.3`, `2.0.0-beta.1`)
|
|
149
|
+
- `{name}` -- the package name from `package.json`
|
|
150
|
+
|
|
151
|
+
Default: `"v{version}"`
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
```yaml
|
|
155
|
+
tag_format: "v{version}" # -> v1.2.3
|
|
156
|
+
tag_format: "release-{version}" # -> release-1.2.3
|
|
157
|
+
tag_format: "{name}-v{version}" # -> my-app-v1.2.3
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### `tag_format_package`
|
|
161
|
+
|
|
162
|
+
Template for sub-package tags in a monorepo.
|
|
163
|
+
|
|
164
|
+
Default: `"{name}/v{version}"`
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
```yaml
|
|
168
|
+
tag_format_package: "{name}/v{version}" # -> @acme/core/v1.2.3
|
|
169
|
+
tag_format_package: "{name}@{version}" # -> @acme/core@1.2.3 (semantic-release compat)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
To migrate from semantic-release's tag format, set `tag_format_package: "{name}@{version}"`.
|
|
173
|
+
|
|
174
|
+
#### `plugins`
|
|
175
|
+
|
|
176
|
+
Ordered list of plugins to execute. Each plugin runs its `prepare` phase, then its `publish` phase. Each plugin accepts an `options` object for customization.
|
|
177
|
+
|
|
178
|
+
Default: `[changelog, npm, git-commit, git-tag]`
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
plugins:
|
|
182
|
+
- name: changelog
|
|
183
|
+
options:
|
|
184
|
+
filename: CHANGELOG.md # output file per package (default: CHANGELOG.md)
|
|
185
|
+
preview_lines: 20 # max lines shown in dry-run (default: 20)
|
|
186
|
+
|
|
187
|
+
- name: npm
|
|
188
|
+
options:
|
|
189
|
+
access: public # npm access level (default: "public")
|
|
190
|
+
registry: https://registry.npmjs.org # custom registry URL
|
|
191
|
+
tag: next # dist-tag override (default: auto from prerelease channel)
|
|
192
|
+
publish_args: # extra args passed to the publish command
|
|
193
|
+
- "--otp=123456"
|
|
194
|
+
package_manager: yarn # force specific PM (default: auto-detect)
|
|
195
|
+
|
|
196
|
+
- name: exec
|
|
197
|
+
options:
|
|
198
|
+
# Run for all packages (omit `packages` to run for all)
|
|
199
|
+
prepare_cmd: "echo Releasing {name} v{version}"
|
|
200
|
+
# Or filter to specific packages:
|
|
201
|
+
# packages: ["@acme/core"]
|
|
202
|
+
|
|
203
|
+
# Multiple exec blocks for different packages:
|
|
204
|
+
# - name: exec
|
|
205
|
+
# options:
|
|
206
|
+
# packages: ["my-rust-project"]
|
|
207
|
+
# prepare_cmd: "sed -i'' -e 's/^version = .*/version = \"{version}\"/' Cargo.toml"
|
|
208
|
+
# - name: exec
|
|
209
|
+
# options:
|
|
210
|
+
# packages: ["@acme/*"]
|
|
211
|
+
# publish_cmd: "deploy.sh {name} {version}"
|
|
212
|
+
|
|
213
|
+
- name: git-commit
|
|
214
|
+
options:
|
|
215
|
+
# Commit message template. Placeholders:
|
|
216
|
+
# {releases} - comma-separated list: "@acme/core@1.1.0, @acme/utils@1.0.1"
|
|
217
|
+
# {summary} - one per line: " - @acme/core 1.0.0 -> 1.1.0"
|
|
218
|
+
# {count} - number of packages released
|
|
219
|
+
message: "chore(release): {releases} [skip ci]"
|
|
220
|
+
push: false # push after commit (default: false)
|
|
221
|
+
remote: origin # git remote (default: "origin")
|
|
222
|
+
paths: # paths to stage (default: ["."])
|
|
223
|
+
- "."
|
|
224
|
+
|
|
225
|
+
- name: git-tag
|
|
226
|
+
options:
|
|
227
|
+
push: false # push tags to remote after creation (default: false)
|
|
228
|
+
remote: origin # git remote to push to (default: "origin")
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Plugin | Prepare | Publish |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `changelog` | Generates/updates changelog per package (parallel) | -- |
|
|
234
|
+
| `npm` | Updates `package.json` versions (auto-detects npm/yarn/pnpm) | Publishes packages (parallel within dependency levels) |
|
|
235
|
+
| `exec` | Runs custom shell command per package | Runs custom shell command per package |
|
|
236
|
+
| `git-commit` | -- | Stages changed files, commits with release message, optionally pushes |
|
|
237
|
+
| `git-tag` | -- | Creates annotated git tags, optionally pushes |
|
|
238
|
+
|
|
239
|
+
The default plugin order ensures: changelogs and version bumps are written first, then committed, then tagged.
|
|
240
|
+
|
|
241
|
+
#### `packages`
|
|
242
|
+
|
|
243
|
+
Optional list of glob patterns to include. When set, only packages whose name matches at least one pattern are released. Supports `*`, `?`, `[...]`, and `{a,b}` alternation.
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
# Only release packages in the @acme scope
|
|
247
|
+
packages:
|
|
248
|
+
- "@acme/*"
|
|
249
|
+
|
|
250
|
+
# Release specific packages
|
|
251
|
+
packages:
|
|
252
|
+
- "@acme/core"
|
|
253
|
+
- "@acme/utils"
|
|
254
|
+
|
|
255
|
+
# Multiple scopes
|
|
256
|
+
packages:
|
|
257
|
+
- "{@acme/*,@tools/*}"
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Default: all discovered packages.
|
|
261
|
+
|
|
262
|
+
#### `exclude`
|
|
263
|
+
|
|
264
|
+
List of glob patterns to exclude from releasing. Applied after `packages`.
|
|
265
|
+
|
|
266
|
+
```yaml
|
|
267
|
+
exclude:
|
|
268
|
+
- my-monorepo-root
|
|
269
|
+
- "@acme/internal-*"
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Monorepo Structure
|
|
273
|
+
|
|
274
|
+
super-release discovers packages by finding `package.json` files recursively (skipping `node_modules`, `.git`, `dist`, `build`). Each commit is associated to a package based on which files it changed.
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
my-monorepo/
|
|
278
|
+
package.json <- root package (tags: v1.0.0)
|
|
279
|
+
.release.yaml
|
|
280
|
+
packages/
|
|
281
|
+
core/
|
|
282
|
+
package.json <- @acme/core (tags: @acme/core/v1.0.0)
|
|
283
|
+
src/
|
|
284
|
+
utils/
|
|
285
|
+
package.json <- @acme/utils (tags: @acme/utils/v1.0.0)
|
|
286
|
+
src/
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Dependency-aware publishing**: The npm plugin builds a dependency graph from `dependencies`, `devDependencies`, and `peerDependencies`. Packages are published in topological order (dependencies before dependents), and interdependency version ranges are updated automatically (preserving `^`/`~` prefixes).
|
|
290
|
+
|
|
291
|
+
## Performance
|
|
292
|
+
|
|
293
|
+
super-release is designed to be fast:
|
|
294
|
+
|
|
295
|
+
- **Parallel diff computation**: commit diffs are computed across multiple threads (thread-local git repo handles)
|
|
296
|
+
- **Tag-bounded history walk**: only walks commits since the oldest package tag, not the entire history
|
|
297
|
+
- **Single-pass commit collection**: commits are fetched once and partitioned per package
|
|
298
|
+
- **Precomputed file mapping**: file-to-package association is computed once, not per-package
|
|
299
|
+
|
|
300
|
+
Benchmark (2001 commits, 8 packages, Apple Silicon):
|
|
301
|
+
| Scenario | Time |
|
|
302
|
+
|---|---|
|
|
303
|
+
| All commits since initial tag | 0.11s |
|
|
304
|
+
| 100 commits since recent tags | 0.035s |
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { platform } from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const bin = join(
|
|
10
|
+
__dirname,
|
|
11
|
+
platform() === "win32" ? "super-release.exe" : "super-release"
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
execFileSync(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err.status !== undefined) process.exit(err.status);
|
|
18
|
+
console.error(`Failed to run super-release: ${err.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
package/npm/install.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, unlinkSync, chmodSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { tmpdir, platform, arch } from "node:os";
|
|
5
|
+
import { get } from "node:https";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const { version } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
|
|
11
|
+
const REPO = "bowlingx/super-release";
|
|
12
|
+
|
|
13
|
+
const PLATFORM_MAP = {
|
|
14
|
+
"linux-x64": "super-release-linux-x86_64",
|
|
15
|
+
"linux-arm64": "super-release-linux-aarch64",
|
|
16
|
+
"darwin-x64": "super-release-darwin-x86_64",
|
|
17
|
+
"darwin-arm64": "super-release-darwin-aarch64",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function download(url) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
get(url, (res) => {
|
|
23
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
24
|
+
return download(res.headers.location).then(resolve, reject);
|
|
25
|
+
}
|
|
26
|
+
if (res.statusCode !== 200) {
|
|
27
|
+
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
resolve(res);
|
|
31
|
+
}).on("error", reject);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const key = `${platform()}-${arch()}`;
|
|
36
|
+
const artifact = PLATFORM_MAP[key];
|
|
37
|
+
|
|
38
|
+
if (!artifact) {
|
|
39
|
+
console.error(
|
|
40
|
+
`Unsupported platform: ${key}. Supported: ${Object.keys(PLATFORM_MAP).join(", ")}`
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const binDir = join(__dirname, "bin");
|
|
46
|
+
const binPath = join(binDir, "super-release");
|
|
47
|
+
|
|
48
|
+
if (existsSync(binPath)) {
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
mkdirSync(binDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
const url = `https://github.com/${REPO}/releases/download/v${version}/${artifact}.tar.gz`;
|
|
55
|
+
console.log(`Downloading super-release v${version} for ${key}...`);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const response = await download(url);
|
|
59
|
+
const tmpFile = join(tmpdir(), `super-release-${Date.now()}.tar.gz`);
|
|
60
|
+
await pipeline(response, createWriteStream(tmpFile));
|
|
61
|
+
execSync(`tar xzf ${tmpFile} -C ${binDir}`, { stdio: "ignore" });
|
|
62
|
+
unlinkSync(tmpFile);
|
|
63
|
+
chmodSync(binPath, 0o755);
|
|
64
|
+
console.log(`Installed super-release v${version}`);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`Failed to install super-release: ${err.message}`);
|
|
67
|
+
console.error(`You can install manually from: https://github.com/${REPO}/releases/tag/v${version}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "super-release",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A fast semantic-release alternative for monorepos, written in Rust",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"super-release": "npm/bin/super-release.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node npm/install.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"npm/bin",
|
|
15
|
+
"npm/install.js",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/bowlingx/super-release.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"semantic-release",
|
|
24
|
+
"monorepo",
|
|
25
|
+
"release",
|
|
26
|
+
"changelog",
|
|
27
|
+
"conventional-commits"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
|
36
|
+
}
|