tegami 0.0.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/dist/generators/simple.d.mts +6 -0
- package/dist/generators/simple.mjs +17 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.mjs +569 -0
- package/dist/npm-BSE_dtB3.mjs +280 -0
- package/dist/plugins/git.d.mts +19 -0
- package/dist/plugins/git.mjs +56 -0
- package/dist/plugins/github.d.mts +23 -0
- package/dist/plugins/github.mjs +41 -0
- package/dist/providers/cargo.d.mts +32 -0
- package/dist/providers/cargo.mjs +186 -0
- package/dist/providers/npm.d.mts +2 -0
- package/dist/providers/npm.mjs +2 -0
- package/dist/types-DdIMewK9.d.mts +283 -0
- package/dist/workspace-B5_i21S0.mjs +63 -0
- package/package.json +49 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import * as semver from "semver";
|
|
2
|
+
import { SemVer } from "semver";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
//#region src/workspace.d.ts
|
|
6
|
+
/** Package discovered in the workspace. */
|
|
7
|
+
declare abstract class WorkspacePackage {
|
|
8
|
+
abstract readonly name: string;
|
|
9
|
+
abstract readonly path: string;
|
|
10
|
+
abstract readonly manager: string;
|
|
11
|
+
abstract readonly version: string;
|
|
12
|
+
abstract readonly publish: boolean;
|
|
13
|
+
get distTag(): string | undefined;
|
|
14
|
+
get id(): string;
|
|
15
|
+
setVersion?(version: string): void;
|
|
16
|
+
updateDependency?(target: WorkspacePackage, version: string, context: TegamiContext): Awaitable<void>;
|
|
17
|
+
write?(): Awaitable<void>;
|
|
18
|
+
protected updateRange(context: TegamiContext, spec: DependencySpec, next: semver.SemVer): Promise<DependencySpec>;
|
|
19
|
+
}
|
|
20
|
+
/** Dependency graph for discovered workspace packages. */
|
|
21
|
+
declare class PackageGraph {
|
|
22
|
+
private readonly byId;
|
|
23
|
+
private readonly byName;
|
|
24
|
+
constructor(packages?: WorkspacePackage[]);
|
|
25
|
+
getPackages(): WorkspacePackage[];
|
|
26
|
+
/** Get a package by exact id. */
|
|
27
|
+
get(id: string): WorkspacePackage | undefined;
|
|
28
|
+
/** Get packages by id, or every package matching a name. */
|
|
29
|
+
getByName(nameOrId: string): WorkspacePackage[];
|
|
30
|
+
/** scan package into graph, if the package id already exists, replace the existing one in graph */
|
|
31
|
+
add(pkg: WorkspacePackage): void;
|
|
32
|
+
delete(pkg: WorkspacePackage): void;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/context.d.ts
|
|
36
|
+
interface TegamiContext {
|
|
37
|
+
cwd: string;
|
|
38
|
+
changelogDir: string;
|
|
39
|
+
planPath: string;
|
|
40
|
+
options: TegamiOptions;
|
|
41
|
+
plugins: TegamiPlugin[];
|
|
42
|
+
graph: PackageGraph;
|
|
43
|
+
/** error if doesn't exist */
|
|
44
|
+
getRegistryClient(pkgOrId: WorkspacePackage | string): RegistryClient;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/utils/semver.d.ts
|
|
48
|
+
type BumpType = "major" | "minor" | "patch";
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/changelog/parse.d.ts
|
|
51
|
+
interface ChangelogEntry {
|
|
52
|
+
id: string;
|
|
53
|
+
/** file name like `my-change.md` */
|
|
54
|
+
filename: string;
|
|
55
|
+
subject?: string;
|
|
56
|
+
packages: Set<string>;
|
|
57
|
+
type: BumpType;
|
|
58
|
+
title: string;
|
|
59
|
+
content: string;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/draft.d.ts
|
|
63
|
+
/** Per-package options applied when creating publish plans. */
|
|
64
|
+
interface PackageOptions {
|
|
65
|
+
/** npm dist-tag used when publishing. */
|
|
66
|
+
distTag?: string;
|
|
67
|
+
/** Set to false to keep this package out of npm publishing. */
|
|
68
|
+
publish?: boolean;
|
|
69
|
+
}
|
|
70
|
+
interface PackagePlan {
|
|
71
|
+
type: BumpType;
|
|
72
|
+
changelogIds: Set<string>;
|
|
73
|
+
distTag?: string;
|
|
74
|
+
publish: boolean;
|
|
75
|
+
}
|
|
76
|
+
declare class DraftPlan {
|
|
77
|
+
#private;
|
|
78
|
+
private readonly changelogs;
|
|
79
|
+
private readonly packages;
|
|
80
|
+
private readonly context;
|
|
81
|
+
constructor(changelogs: Map<string, ChangelogEntry>, packages: Map<string, PackagePlan>, context: TegamiContext);
|
|
82
|
+
getPackageIds(): string[];
|
|
83
|
+
getPackage(id: string): PackagePlan | undefined;
|
|
84
|
+
setPackage(id: string, plan?: Partial<PackagePlan>): void;
|
|
85
|
+
deletePackage(id: string): boolean;
|
|
86
|
+
getChangelogIds(): string[];
|
|
87
|
+
getChangelog(id: string): ChangelogEntry | undefined;
|
|
88
|
+
setChangelog(id: string, entry: ChangelogEntry): void;
|
|
89
|
+
deleteChangelog(id: string): boolean;
|
|
90
|
+
/** Write the publish plan, update package versions, and consume changelog files. */
|
|
91
|
+
createPublishPlan(): Promise<void>;
|
|
92
|
+
private assertPublishPlanFinished;
|
|
93
|
+
private applyVersionChanges;
|
|
94
|
+
private removeConsumedChangelogs;
|
|
95
|
+
private assertEditable;
|
|
96
|
+
private appendChangelog;
|
|
97
|
+
/** {@link createPublishPlan} but for `await using` syntax */
|
|
98
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/schemas.d.ts
|
|
102
|
+
declare const packageManifestSchema: z.ZodObject<{
|
|
103
|
+
name: z.ZodOptional<z.ZodString>;
|
|
104
|
+
version: z.ZodOptional<z.ZodString>;
|
|
105
|
+
private: z.ZodOptional<z.ZodBoolean>;
|
|
106
|
+
publishConfig: z.ZodOptional<z.ZodObject<{
|
|
107
|
+
access: z.ZodOptional<z.ZodEnum<{
|
|
108
|
+
public: "public";
|
|
109
|
+
restricted: "restricted";
|
|
110
|
+
}>>;
|
|
111
|
+
registry: z.ZodOptional<z.ZodString>;
|
|
112
|
+
tag: z.ZodOptional<z.ZodString>;
|
|
113
|
+
}, z.core.$loose>>;
|
|
114
|
+
workspaces: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodPipe<z.ZodObject<{
|
|
115
|
+
packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
116
|
+
}, z.core.$loose>, z.ZodTransform<string[], {
|
|
117
|
+
[x: string]: unknown;
|
|
118
|
+
packages?: string[] | undefined;
|
|
119
|
+
}>>]>, z.ZodArray<z.ZodString>>>;
|
|
120
|
+
dependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
121
|
+
devDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
122
|
+
peerDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
123
|
+
optionalDependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
124
|
+
}, z.core.$loose>;
|
|
125
|
+
type PackageManifest = z.infer<typeof packageManifestSchema>;
|
|
126
|
+
/** the persisted plan data for actual publishing */
|
|
127
|
+
declare const planStoreSchema: z.ZodCodec<z.ZodString, z.ZodObject<{
|
|
128
|
+
id: z.ZodString;
|
|
129
|
+
createdAt: z.ZodISODateTime;
|
|
130
|
+
changelogs: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
131
|
+
filename: z.ZodString;
|
|
132
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
133
|
+
packages: z.ZodArray<z.ZodString>;
|
|
134
|
+
type: z.ZodEnum<{
|
|
135
|
+
major: "major";
|
|
136
|
+
minor: "minor";
|
|
137
|
+
patch: "patch";
|
|
138
|
+
}>;
|
|
139
|
+
title: z.ZodString;
|
|
140
|
+
content: z.ZodString;
|
|
141
|
+
}, z.core.$strip>>;
|
|
142
|
+
packages: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
143
|
+
type: z.ZodEnum<{
|
|
144
|
+
major: "major";
|
|
145
|
+
minor: "minor";
|
|
146
|
+
patch: "patch";
|
|
147
|
+
}>;
|
|
148
|
+
changelogIds: z.ZodCodec<z.ZodArray<z.ZodString>, z.ZodSet<z.ZodString>>;
|
|
149
|
+
distTag: z.ZodOptional<z.ZodString>;
|
|
150
|
+
publish: z.ZodBoolean;
|
|
151
|
+
}, z.core.$strip>>;
|
|
152
|
+
}, z.core.$strip>>;
|
|
153
|
+
type PlanStore = z.output<typeof planStoreSchema>;
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/publish.d.ts
|
|
156
|
+
interface PublishOptions {
|
|
157
|
+
/** Validate the publish plan without publishing packages, creating tags, or running release plugins. */
|
|
158
|
+
dryRun?: boolean;
|
|
159
|
+
}
|
|
160
|
+
interface PublishResult {
|
|
161
|
+
/** Path to the publish plan that was executed. */
|
|
162
|
+
planPath: string;
|
|
163
|
+
state: "success" | "failed";
|
|
164
|
+
packages: PackagePublishResult[];
|
|
165
|
+
/** the persisted plan object. This is not a public API, can be changed without notice */
|
|
166
|
+
_rawPlan: PlanStore;
|
|
167
|
+
}
|
|
168
|
+
type PackagePublishResult = ({
|
|
169
|
+
state: "failed";
|
|
170
|
+
error?: string;
|
|
171
|
+
} | {
|
|
172
|
+
state: "success";
|
|
173
|
+
}) & {
|
|
174
|
+
id: string;
|
|
175
|
+
name: string;
|
|
176
|
+
version: string;
|
|
177
|
+
distTag: string | undefined; /** added by the `git` plugin */
|
|
178
|
+
gitTag?: string;
|
|
179
|
+
changelogs: ChangelogEntry[];
|
|
180
|
+
};
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/providers/npm.d.ts
|
|
183
|
+
declare class NpmPackage extends WorkspacePackage {
|
|
184
|
+
readonly path: string;
|
|
185
|
+
readonly manifest: PackageManifest;
|
|
186
|
+
readonly manager = "npm";
|
|
187
|
+
constructor(path: string, manifest: PackageManifest);
|
|
188
|
+
get name(): string;
|
|
189
|
+
get version(): string;
|
|
190
|
+
get publish(): boolean;
|
|
191
|
+
get distTag(): string | undefined;
|
|
192
|
+
setVersion(version: string): void;
|
|
193
|
+
updateDependency(target: WorkspacePackage, version: string, context: TegamiContext): Promise<void>;
|
|
194
|
+
write(): Promise<void>;
|
|
195
|
+
}
|
|
196
|
+
type NpmClient = "npm" | "pnpm";
|
|
197
|
+
declare class NpmRegistryClient implements RegistryClient {
|
|
198
|
+
#private;
|
|
199
|
+
private readonly cwd;
|
|
200
|
+
private readonly npmClient;
|
|
201
|
+
private readonly graph;
|
|
202
|
+
readonly id = "npm";
|
|
203
|
+
constructor(cwd: string, npmClient: NpmClient | undefined, graph: {
|
|
204
|
+
get(id: string): WorkspacePackage | undefined;
|
|
205
|
+
});
|
|
206
|
+
supports(pkg: WorkspacePackage): boolean;
|
|
207
|
+
packageVersionExists(pkg: WorkspacePackage, version: string): Promise<boolean>;
|
|
208
|
+
publish(pkg: WorkspacePackage, options?: {
|
|
209
|
+
distTag?: string;
|
|
210
|
+
}): Promise<void>;
|
|
211
|
+
publishPlanStatus(plan: PlanStore): Promise<PublishPlanStatus>;
|
|
212
|
+
private resolveClient;
|
|
213
|
+
}
|
|
214
|
+
declare function npm(client?: NpmClient): TegamiPlugin;
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/types.d.ts
|
|
217
|
+
/** Generates changelog content for a package release. */
|
|
218
|
+
interface LogGenerator {
|
|
219
|
+
generate(this: TegamiContext, opts: {
|
|
220
|
+
packageName: string;
|
|
221
|
+
version: string;
|
|
222
|
+
changelogs: ChangelogEntry[];
|
|
223
|
+
}): string | Promise<string>;
|
|
224
|
+
}
|
|
225
|
+
interface TegamiOptions {
|
|
226
|
+
/** Workspace root. Defaults to the current working directory. */
|
|
227
|
+
cwd?: string;
|
|
228
|
+
/** Directory containing pending changelog markdown files. */
|
|
229
|
+
changelogDir?: string;
|
|
230
|
+
/** Path to the publish plan file. */
|
|
231
|
+
planPath?: string;
|
|
232
|
+
/** Changelog generator used when creating a publish plan. */
|
|
233
|
+
generator?: LogGenerator;
|
|
234
|
+
/** Per-package release and publish options keyed by package name. */
|
|
235
|
+
packages?: Record<string, PackageOptions>;
|
|
236
|
+
plugins?: TegamiPluginOption[];
|
|
237
|
+
/** Package manager command used for npm registry operations. */
|
|
238
|
+
npmClient?: NpmClient;
|
|
239
|
+
}
|
|
240
|
+
type TegamiPluginOption = TegamiPlugin | TegamiPluginOption[];
|
|
241
|
+
interface TegamiPlugin {
|
|
242
|
+
name: string;
|
|
243
|
+
enforce?: "pre" | "default" | "post";
|
|
244
|
+
/** when Tegami initializes */
|
|
245
|
+
init?(this: TegamiContext): Awaitable<void>;
|
|
246
|
+
/** Resolve workspace packages and dependency metadata into the shared graph. */
|
|
247
|
+
resolve?(this: TegamiContext): Awaitable<void>;
|
|
248
|
+
/** Register registry clients used to handle packages for different package managers. */
|
|
249
|
+
createRegistryClient?(this: TegamiContext): Awaitable<RegistryClient | RegistryClient[] | void | undefined>;
|
|
250
|
+
/** Called after Tegami builds the initial draft plan and before it is returned. */
|
|
251
|
+
initPlan?(this: TegamiContext, plan: DraftPlan): Awaitable<DraftPlan | void | undefined>;
|
|
252
|
+
/** Called after publishing finishes. */
|
|
253
|
+
afterPublish?(this: TegamiContext & {
|
|
254
|
+
publishOptions: PublishOptions;
|
|
255
|
+
}, result: PublishResult): Awaitable<PublishResult | void | undefined>;
|
|
256
|
+
/**
|
|
257
|
+
* @param pkg - the package that referenced the dependency
|
|
258
|
+
* @param spec - the referenced dependency & its range
|
|
259
|
+
* @param target - the target version to update to
|
|
260
|
+
* @returns fallback to the default behaviour if `undefined`, otherwise replace with updated spec (can reuse the same instance, as long as a value is returned).
|
|
261
|
+
*/
|
|
262
|
+
onUpdateRange?(this: TegamiContext, pkg: WorkspacePackage, spec: DependencySpec, target: SemVer): Awaitable<DependencySpec | void | undefined>;
|
|
263
|
+
}
|
|
264
|
+
type Awaitable<T> = T | Promise<T>;
|
|
265
|
+
interface PublishPlanStatus {
|
|
266
|
+
state: "pending" | "success";
|
|
267
|
+
error?: string;
|
|
268
|
+
}
|
|
269
|
+
interface RegistryClient {
|
|
270
|
+
id: string;
|
|
271
|
+
supports?(pkg: WorkspacePackage): boolean;
|
|
272
|
+
packageVersionExists(pkg: WorkspacePackage, version: string): Promise<boolean>;
|
|
273
|
+
publish(pkg: WorkspacePackage, options?: {
|
|
274
|
+
distTag?: string;
|
|
275
|
+
}): Promise<void>;
|
|
276
|
+
publishPlanStatus(plan: PlanStore): Promise<PublishPlanStatus>;
|
|
277
|
+
}
|
|
278
|
+
interface DependencySpec {
|
|
279
|
+
name: string;
|
|
280
|
+
range: string;
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
export { PackageOptions as _, TegamiOptions as a, PackageGraph as b, NpmClient as c, npm as d, PackagePublishResult as f, DraftPlan as g, PlanStore as h, RegistryClient as i, NpmPackage as l, PublishResult as m, LogGenerator as n, TegamiPlugin as o, PublishOptions as p, PublishPlanStatus as r, TegamiPluginOption as s, Awaitable as t, NpmRegistryClient as u, PackagePlan as v, WorkspacePackage as x, TegamiContext as y };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as semver from "semver";
|
|
2
|
+
//#region src/utils/error.ts
|
|
3
|
+
function isNodeError(error) {
|
|
4
|
+
return error instanceof Error && "code" in error;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/workspace.ts
|
|
8
|
+
/** Package discovered in the workspace. */
|
|
9
|
+
var WorkspacePackage = class {
|
|
10
|
+
get distTag() {}
|
|
11
|
+
get id() {
|
|
12
|
+
return `${this.manager}:${this.name}`;
|
|
13
|
+
}
|
|
14
|
+
async updateRange(context, spec, next) {
|
|
15
|
+
for (const plugin of context.plugins) {
|
|
16
|
+
const result = await plugin.onUpdateRange?.call(context, this, spec, next);
|
|
17
|
+
if (result) return result;
|
|
18
|
+
}
|
|
19
|
+
if (!semver.validRange(spec.range)) return spec;
|
|
20
|
+
if (new semver.Range(spec.range).test(next)) return spec;
|
|
21
|
+
spec.range = next.format();
|
|
22
|
+
return spec;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Dependency graph for discovered workspace packages. */
|
|
26
|
+
var PackageGraph = class {
|
|
27
|
+
byId = /* @__PURE__ */ new Map();
|
|
28
|
+
byName = /* @__PURE__ */ new Map();
|
|
29
|
+
constructor(packages = []) {
|
|
30
|
+
for (const pkg of packages) this.add(pkg);
|
|
31
|
+
}
|
|
32
|
+
getPackages() {
|
|
33
|
+
return Array.from(this.byId.values());
|
|
34
|
+
}
|
|
35
|
+
/** Get a package by exact id. */
|
|
36
|
+
get(id) {
|
|
37
|
+
return this.byId.get(id);
|
|
38
|
+
}
|
|
39
|
+
/** Get packages by id, or every package matching a name. */
|
|
40
|
+
getByName(nameOrId) {
|
|
41
|
+
const exact = this.byId.get(nameOrId);
|
|
42
|
+
if (exact) return [exact];
|
|
43
|
+
return this.byName.get(nameOrId) ?? [];
|
|
44
|
+
}
|
|
45
|
+
/** scan package into graph, if the package id already exists, replace the existing one in graph */
|
|
46
|
+
add(pkg) {
|
|
47
|
+
const existing = this.byId.get(pkg.id);
|
|
48
|
+
if (existing) this.delete(existing);
|
|
49
|
+
this.byId.set(pkg.id, pkg);
|
|
50
|
+
const named = this.byName.get(pkg.name);
|
|
51
|
+
if (named) named.push(pkg);
|
|
52
|
+
else this.byName.set(pkg.name, [pkg]);
|
|
53
|
+
}
|
|
54
|
+
delete(pkg) {
|
|
55
|
+
this.byId.delete(pkg.id);
|
|
56
|
+
const named = this.byName.get(pkg.name);
|
|
57
|
+
if (!named) return;
|
|
58
|
+
const index = named.findIndex((item) => item.id === pkg.id);
|
|
59
|
+
if (index !== -1) named.splice(index, 1);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { WorkspacePackage as n, isNodeError as r, PackageGraph as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tegami",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Utility for package versioning & publish",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Fuma Nama",
|
|
7
|
+
"repository": "github:fuma-nama/tegami",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.mjs",
|
|
14
|
+
"./generators/simple": "./dist/generators/simple.mjs",
|
|
15
|
+
"./plugins/git": "./dist/plugins/git.mjs",
|
|
16
|
+
"./plugins/github": "./dist/plugins/github.mjs",
|
|
17
|
+
"./providers/cargo": "./dist/providers/cargo.mjs",
|
|
18
|
+
"./providers/npm": "./dist/providers/npm.mjs",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"types:check": "tsc --noEmit",
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"dev": "tsdown --watch"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"js-yaml": "^4.2.0",
|
|
31
|
+
"mdast-util-from-markdown": "^2.0.3",
|
|
32
|
+
"mdast-util-to-markdown": "^2.1.2",
|
|
33
|
+
"package-manager-detector": "^1.6.0",
|
|
34
|
+
"semver": "^7.8.4",
|
|
35
|
+
"smol-toml": "^1.6.1",
|
|
36
|
+
"tinyexec": "^1.2.4",
|
|
37
|
+
"tinyglobby": "^0.2.17",
|
|
38
|
+
"zod": "^4.4.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@repo/typescript-config": "workspace:*",
|
|
42
|
+
"@types/js-yaml": "^4.0.9",
|
|
43
|
+
"@types/mdast": "^4.0.4",
|
|
44
|
+
"@types/node": "^25.5.0",
|
|
45
|
+
"@types/semver": "^7.7.1",
|
|
46
|
+
"tsdown": "^0.22.2",
|
|
47
|
+
"typescript": "6.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|