@genesislcap/event-type-codegen 15.19.0 → 15.19.1

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.
@@ -1,105 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
-
3
- export type KotlinEnumMap = Map<string, readonly string[]>;
4
-
5
- const KOTLIN_FILE_EXTENSION = '.kt';
6
-
7
- /** Any generated DAO tree Kotlin (entities, enums, xref, etc.). */
8
- export const isGeneratedDaoTreeFile = (filePath: string): boolean => {
9
- const normalized = filePath.replace(/\\/g, '/');
10
- return /\/gen\/dao\//i.test(normalized) && normalized.endsWith(KOTLIN_FILE_EXTENSION);
11
- };
12
-
13
- const parseEnumMembers = (body: string): string[] =>
14
- body
15
- .split(',')
16
- .map((part) => part.replace(/\/\/.*$/, '').trim())
17
- .map((part) => part.split(/\s+/)[0]?.trim() ?? '')
18
- .filter((name) => /^\w+$/.test(name));
19
-
20
- /** `enum class Side { BUY, SELL }` in generated DAO sources. */
21
- export const extractEnumClassesFromContent = (content: string): KotlinEnumMap => {
22
- const enums: KotlinEnumMap = new Map();
23
- const enumRegex = /(?:^|\n)\s*(?:public\s+)?enum\s+class\s+(\w+)\s*\{([^}]*)\}/gm;
24
- let match: RegExpExecArray | null;
25
-
26
- while ((match = enumRegex.exec(content)) !== null) {
27
- const name = match[1];
28
- const members = parseEnumMembers(match[2]);
29
- if (members.length > 0) {
30
- enums.set(name, members);
31
- }
32
- }
33
-
34
- return enums;
35
- };
36
-
37
- /**
38
- * Companion `Field` metadata on generated entities, e.g.
39
- * `values = "STANDARD PREMIUM TRIAL"` on `NonNullable<Tier>(Field(… type = Field.Type.ENUM …))`.
40
- */
41
- export const extractCompanionFieldEnumValues = (content: string): KotlinEnumMap => {
42
- const enums: KotlinEnumMap = new Map();
43
- const blockRegex =
44
- /public\s+val\s+\w+:\s*NonNullable<(\w+)>\s*=\s*NonNullable<\1>\(Field\(([\s\S]*?)\)\)/g;
45
- let match: RegExpExecArray | null;
46
-
47
- while ((match = blockRegex.exec(content)) !== null) {
48
- const enumTypeName = match[1];
49
- const fieldBody = match[2];
50
- if (!fieldBody.includes('Field.Type.ENUM')) continue;
51
-
52
- const valuesMatch = fieldBody.match(/values\s*=\s*"([^"]+)"/);
53
- if (!valuesMatch) continue;
54
-
55
- const literals = valuesMatch[1]
56
- .trim()
57
- .split(/\s+/)
58
- .filter((value) => value.length > 0);
59
-
60
- if (literals.length > 0) {
61
- enums.set(enumTypeName, literals);
62
- }
63
- }
64
-
65
- return enums;
66
- };
67
-
68
- const mergeEnumMaps = (into: KotlinEnumMap, from: KotlinEnumMap): void => {
69
- for (const [name, values] of from) {
70
- if (!into.has(name)) {
71
- into.set(name, values);
72
- }
73
- }
74
- };
75
-
76
- export const parseGeneratedEnumsFromSources = (files: string[]): KotlinEnumMap => {
77
- const enums: KotlinEnumMap = new Map();
78
-
79
- for (const filePath of files) {
80
- if (!isGeneratedDaoTreeFile(filePath)) continue;
81
- const content = readFileSync(filePath, 'utf8');
82
- mergeEnumMaps(enums, extractEnumClassesFromContent(content));
83
- mergeEnumMaps(enums, extractCompanionFieldEnumValues(content));
84
- }
85
-
86
- return enums;
87
- };
88
-
89
- export const kotlinEnumSimpleName = (kotlinType: string): string => {
90
- const bare = kotlinType.replace(/\?$/, '').trim();
91
- return bare.includes('.') ? (bare.split('.').pop() ?? bare) : bare;
92
- };
93
-
94
- export const kotlinEnumTypeToTs = (kotlinType: string, enums: KotlinEnumMap): string | null => {
95
- const simpleName = kotlinEnumSimpleName(kotlinType);
96
- const values = enums.get(simpleName);
97
- if (!values || values.length === 0) return null;
98
- return values.map((value) => `'${value}'`).join(' | ');
99
- };
100
-
101
- /** Use generated enum type name when declared (paired with `renderEnumExports`). */
102
- export const kotlinEnumTypeRef = (kotlinType: string, enums: KotlinEnumMap): string | null => {
103
- const simpleName = kotlinEnumSimpleName(kotlinType);
104
- return enums.has(simpleName) ? simpleName : null;
105
- };
@@ -1,181 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
-
3
- import { parseKotlinProperty, splitTopLevelArgs, stripKotlinComments } from './kotlin-parse-utils';
4
- import { handlerInputToDetailsInterfaceName } from './naming';
5
- import { kotlinEnumTypeRef } from './parse-enums';
6
- import type { KotlinEnumMap } from './parse-enums';
7
- import type { KotlinDto, KotlinProperty, MappedHandler, UnmappedHandler } from './types';
8
-
9
- const PRIMITIVE_TS: Record<string, string> = {
10
- String: 'string',
11
- Int: 'number',
12
- Long: 'number',
13
- Short: 'number',
14
- Byte: 'number',
15
- Double: 'number',
16
- Float: 'number',
17
- Boolean: 'boolean',
18
- BigDecimal: 'string',
19
- // Genesis wire JSON_TYPE for DATE/DATETIME is integer (epoch millis).
20
- DateTime: 'number',
21
- Instant: 'number',
22
- LocalDate: 'number',
23
- LocalDateTime: 'number',
24
- };
25
-
26
- const DATA_CLASS_START = /^\s*(?:data\s+)?class\s+(\w+)\s*(?:\([^)]*\))?\s*(?:\([^)]*\)|\{)/;
27
-
28
- const extractDataClasses = (content: string): { name: string; properties: KotlinProperty[] }[] => {
29
- const results: { name: string; properties: KotlinProperty[] }[] = [];
30
- const classRegex = /(?:^|\n)\s*(?:data\s+)?class\s+(\w+)\s*\(/gm;
31
- let match: RegExpExecArray | null;
32
-
33
- while ((match = classRegex.exec(content)) !== null) {
34
- const name = match[1];
35
- const openParenIndex = match.index + match[0].length - 1;
36
- let depth = 0;
37
- let closeIndex = -1;
38
- for (let i = openParenIndex; i < content.length; i += 1) {
39
- const ch = content[i];
40
- if (ch === '(') depth += 1;
41
- if (ch === ')') {
42
- depth -= 1;
43
- if (depth === 0) {
44
- closeIndex = i;
45
- break;
46
- }
47
- }
48
- }
49
- if (closeIndex === -1) continue;
50
-
51
- const body = content.slice(openParenIndex + 1, closeIndex);
52
- const properties = splitTopLevelArgs(body)
53
- .map((segment) => parseKotlinProperty(segment))
54
- .filter((p): p is KotlinProperty => p != null);
55
-
56
- if (properties.length > 0) {
57
- results.push({ name, properties });
58
- }
59
- }
60
-
61
- return results;
62
- };
63
-
64
- export const parseDtosFromSources = (files: string[]): Map<string, KotlinDto> => {
65
- const dtos = new Map<string, KotlinDto>();
66
-
67
- for (const filePath of files) {
68
- const content = stripKotlinComments(readFileSync(filePath, 'utf8'));
69
- for (const parsed of extractDataClasses(content)) {
70
- dtos.set(parsed.name, {
71
- name: parsed.name,
72
- properties: parsed.properties,
73
- filePath,
74
- });
75
- }
76
- }
77
-
78
- return dtos;
79
- };
80
-
81
- const HANDLER_REGEX =
82
- /(?:eventHandler|contextEventHandler)\s*<\s*([^<>,]+?)(?:\s*,\s*[^>]+)?\s*>\s*\(\s*(?:name\s*=\s*)?"([^"]+)"[^)]*\)/g;
83
-
84
- const HANDLER_INDEX_INPUT = /^(\w+)\.(By\w+)$/;
85
-
86
- const unmappedReason = (inputType: string): string | null => {
87
- const trimmed = inputType.trim();
88
- if (trimmed === 'Unit') return 'Unit input';
89
- if (HANDLER_INDEX_INPUT.test(trimmed)) return null;
90
- if (trimmed.includes('.')) return 'Non-DTO type (DAO/index/nested qualified type)';
91
- return null;
92
- };
93
-
94
- export const parseHandlersFromSources = (
95
- files: string[],
96
- dtos: Map<string, KotlinDto>,
97
- ): { mapped: MappedHandler[]; unmapped: UnmappedHandler[] } => {
98
- const mapped: MappedHandler[] = [];
99
- const unmapped: UnmappedHandler[] = [];
100
- const seenEvents = new Set<string>();
101
-
102
- for (const filePath of files) {
103
- const stripped = stripKotlinComments(readFileSync(filePath, 'utf8'));
104
-
105
- const tryMatch = (regex: RegExp) => {
106
- let handlerMatch: RegExpExecArray | null;
107
- regex.lastIndex = 0;
108
- while ((handlerMatch = regex.exec(stripped)) !== null) {
109
- const inputType = handlerMatch[1].trim();
110
- const handlerName = handlerMatch[2].trim();
111
- const eventName = handlerName.startsWith('EVENT_') ? handlerName : `EVENT_${handlerName}`;
112
-
113
- const skipReason = unmappedReason(inputType);
114
- if (skipReason) {
115
- unmapped.push({
116
- handlerName,
117
- inputType,
118
- reason: skipReason,
119
- sourceFile: filePath,
120
- });
121
- continue;
122
- }
123
-
124
- if (!dtos.has(inputType)) {
125
- unmapped.push({
126
- handlerName,
127
- inputType,
128
- reason: 'No matching data class or generated DAO',
129
- sourceFile: filePath,
130
- });
131
- continue;
132
- }
133
-
134
- if (seenEvents.has(eventName)) continue;
135
- seenEvents.add(eventName);
136
-
137
- mapped.push({
138
- eventName,
139
- inputType,
140
- detailsInterface: handlerInputToDetailsInterfaceName(inputType),
141
- sourceFile: filePath,
142
- });
143
- }
144
- };
145
-
146
- tryMatch(HANDLER_REGEX);
147
- }
148
-
149
- mapped.sort((a, b) => a.eventName.localeCompare(b.eventName));
150
- unmapped.sort((a, b) => a.handlerName.localeCompare(b.handlerName));
151
-
152
- return { mapped, unmapped };
153
- };
154
-
155
- export const kotlinTypeToTs = (
156
- kotlinType: string,
157
- dtos: Map<string, KotlinDto>,
158
- enums: KotlinEnumMap = new Map(),
159
- ): string => {
160
- const listMatch = kotlinType.match(/^(?:List|MutableList|Set|MutableSet)<(.+)>$/);
161
- if (listMatch) {
162
- return `${kotlinTypeToTs(listMatch[1].trim(), dtos, enums)}[]`;
163
- }
164
-
165
- if (PRIMITIVE_TS[kotlinType]) {
166
- return PRIMITIVE_TS[kotlinType];
167
- }
168
-
169
- const enumTs = kotlinEnumTypeRef(kotlinType, enums);
170
- if (enumTs) {
171
- return enumTs;
172
- }
173
-
174
- if (dtos.has(kotlinType)) {
175
- return handlerInputToDetailsInterfaceName(kotlinType);
176
- }
177
-
178
- return 'unknown';
179
- };
180
-
181
- export { extractDataClasses, DATA_CLASS_START };
@@ -1,327 +0,0 @@
1
- import { eventNameToEventClassName } from './naming';
2
-
3
- /** Wire-named DETAILS property from live event metadata / JSON Schema. */
4
- export type MetadataField = {
5
- /** Already UPPER_SNAKE wire name (e.g. ADDRESS_LINE_1). */
6
- name: string;
7
- /** Emitted TypeScript type (e.g. string, number, Side). */
8
- tsType: string;
9
- optional: boolean;
10
- };
11
-
12
- export type MetadataEventContract = {
13
- eventName: string;
14
- detailsInterface: string;
15
- fields: MetadataField[];
16
- };
17
-
18
- export type MetadataEnumDef = {
19
- name: string;
20
- values: string[];
21
- };
22
-
23
- /**
24
- * Dump shape produced by foundation-header dev capture (FUI-2608).
25
- * Prefer `schema.INBOUND` DETAILS; fall back to `metadata.FIELD`.
26
- */
27
- export type EventMetadataDump = {
28
- capturedAt?: string;
29
- source?: string;
30
- events: Record<
31
- string,
32
- {
33
- metadata?: {
34
- FIELD?: Array<{
35
- NAME: string;
36
- TYPE?: string;
37
- JSON_TYPE?: string;
38
- OPTIONAL?: boolean;
39
- VALID_VALUES?: string;
40
- }>;
41
- NAME?: string;
42
- };
43
- schema?: {
44
- INBOUND?: {
45
- properties?: Record<string, unknown>;
46
- required?: string[];
47
- };
48
- MESSAGE_TYPE?: string;
49
- };
50
- }
51
- >;
52
- };
53
-
54
- const isRecord = (value: unknown): value is Record<string, unknown> =>
55
- typeof value === 'object' && value != null && !Array.isArray(value);
56
-
57
- /** Quote a string for safe emission as a TS string literal / object key. */
58
- export const quoteTsString = (s: string): string =>
59
- `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
60
-
61
- /** Bare identifier when legal; otherwise a quoted string key. */
62
- const tsPropertyKey = (name: string): string =>
63
- /^[A-Za-z_$][\w$]*$/.test(name) ? name : quoteTsString(name);
64
-
65
- /** EVENT_BROKER_INSERT → BrokerInsertDetails */
66
- export const eventNameToDetailsInterfaceName = (eventName: string): string => {
67
- const base = eventName.startsWith('EVENT_') ? eventName.slice('EVENT_'.length) : eventName;
68
- const pascal = base
69
- .toLowerCase()
70
- .split('_')
71
- .filter(Boolean)
72
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
73
- .join('');
74
- return `${pascal}Details`;
75
- };
76
-
77
- /** Build a valid PascalCase TypeScript identifier from an enum name hint. */
78
- const toSafeEnumIdentifier = (enumNameHint: string): string => {
79
- const enumName = enumNameHint
80
- .split(/[^A-Za-z0-9]+/)
81
- .filter(Boolean)
82
- .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
83
- .join('');
84
- if (!enumName) return 'GeneratedEnum';
85
- return /^[A-Za-z]/.test(enumName) ? enumName : `E${enumName}`;
86
- };
87
-
88
- const valuesKey = (values: string[]): string => values.join('\0');
89
-
90
- /**
91
- * Register an enum under a collision-safe name. Same values → reuse; different values →
92
- * disambiguate (`Foo`, `Foo2`, …) and report via `unmapped`.
93
- */
94
- const registerEnum = (
95
- enums: Map<string, string[]>,
96
- preferredName: string,
97
- values: string[],
98
- unmapped: string[],
99
- context: string,
100
- ): string => {
101
- const existing = enums.get(preferredName);
102
- if (!existing) {
103
- enums.set(preferredName, values);
104
- return preferredName;
105
- }
106
- if (valuesKey(existing) === valuesKey(values)) {
107
- return preferredName;
108
- }
109
-
110
- let n = 2;
111
- let candidate = `${preferredName}${n}`;
112
- while (enums.has(candidate) && valuesKey(enums.get(candidate)!) !== valuesKey(values)) {
113
- n += 1;
114
- candidate = `${preferredName}${n}`;
115
- }
116
- if (!enums.has(candidate)) {
117
- enums.set(candidate, values);
118
- unmapped.push(`${context}: enum name collision for ${preferredName}; emitted as ${candidate}`);
119
- }
120
- return candidate;
121
- };
122
-
123
- const jsonSchemaTypeToTs = (
124
- propSchema: Record<string, unknown>,
125
- enums: Map<string, string[]>,
126
- enumNameHint: string,
127
- unmapped: string[],
128
- context: string,
129
- ): string => {
130
- if (Array.isArray(propSchema.enum) && propSchema.enum.every((v) => typeof v === 'string')) {
131
- const values = propSchema.enum as string[];
132
- const safeName = toSafeEnumIdentifier(enumNameHint);
133
- return registerEnum(enums, safeName, values, unmapped, context);
134
- }
135
-
136
- if (Array.isArray(propSchema.oneOf)) {
137
- const nonNull = (propSchema.oneOf as unknown[]).find(
138
- (branch) => isRecord(branch) && branch.type !== 'null',
139
- );
140
- if (isRecord(nonNull)) {
141
- return jsonSchemaTypeToTs(nonNull, enums, enumNameHint, unmapped, context);
142
- }
143
- }
144
-
145
- const type = propSchema.type;
146
- if (type === 'integer' || type === 'number') return 'number';
147
- if (type === 'boolean') return 'boolean';
148
- if (type === 'array') {
149
- const items = isRecord(propSchema.items) ? propSchema.items : {};
150
- return `${jsonSchemaTypeToTs(items, enums, enumNameHint, unmapped, context)}[]`;
151
- }
152
- if (type === 'object') return 'Record<string, unknown>';
153
- return 'string';
154
- };
155
-
156
- const metadataJsonTypeToTs = (jsonType?: string, fieldType?: string): string => {
157
- const jt = (jsonType ?? '').toLowerCase();
158
- if (jt === 'integer' || jt === 'number') return 'number';
159
- if (jt === 'boolean') return 'boolean';
160
- if (
161
- (fieldType ?? '').toUpperCase() === 'DATETIME' ||
162
- (fieldType ?? '').toUpperCase() === 'DATE'
163
- ) {
164
- return 'number';
165
- }
166
- return 'string';
167
- };
168
-
169
- const extractDetailsSchema = (
170
- inbound: Record<string, unknown> | undefined,
171
- ): { properties: Record<string, unknown>; required: Set<string> } | null => {
172
- if (!inbound) return null;
173
- const rootProps = isRecord(inbound.properties) ? inbound.properties : undefined;
174
- if (!rootProps) return null;
175
-
176
- // Event inbound is usually { DETAILS: { type: object, properties: {...}, required: [...] } }
177
- const details = rootProps.DETAILS;
178
- if (isRecord(details) && isRecord(details.properties)) {
179
- const required = Array.isArray(details.required)
180
- ? new Set(details.required.filter((r): r is string => typeof r === 'string'))
181
- : new Set<string>();
182
- return { properties: details.properties, required };
183
- }
184
-
185
- // Some schemas flatten DETAILS properties at the inbound root.
186
- const required = Array.isArray(inbound.required)
187
- ? new Set(inbound.required.filter((r): r is string => typeof r === 'string'))
188
- : new Set<string>();
189
- return { properties: rootProps, required };
190
- };
191
-
192
- /**
193
- * Parse a header-captured event-metadata.json dump into per-event contracts.
194
- */
195
- export const parseEventMetadataDump = (
196
- dump: EventMetadataDump,
197
- ): { contracts: MetadataEventContract[]; enums: MetadataEnumDef[]; unmapped: string[] } => {
198
- const contracts: MetadataEventContract[] = [];
199
- const enums = new Map<string, string[]>();
200
- const unmapped: string[] = [];
201
-
202
- const eventNames = Object.keys(dump.events ?? {}).sort((a, b) => a.localeCompare(b));
203
-
204
- for (const eventName of eventNames) {
205
- if (!eventName.startsWith('EVENT_')) {
206
- unmapped.push(`${eventName}: not an EVENT_* handler`);
207
- continue;
208
- }
209
-
210
- const entry = dump.events[eventName];
211
- const inbound = isRecord(entry?.schema?.INBOUND)
212
- ? (entry.schema.INBOUND as Record<string, unknown>)
213
- : undefined;
214
- const detailsSchema = extractDetailsSchema(inbound);
215
-
216
- const fields: MetadataField[] = [];
217
-
218
- if (detailsSchema) {
219
- for (const [fieldName, rawProp] of Object.entries(detailsSchema.properties)) {
220
- if (fieldName === 'MESSAGE_TYPE' || fieldName === 'SOURCE_REF') continue;
221
- if (!isRecord(rawProp)) continue;
222
- const optional = !detailsSchema.required.has(fieldName);
223
- const enumNameHint = eventName.startsWith('EVENT_')
224
- ? `${eventName.slice('EVENT_'.length)}_${fieldName}`
225
- : `${eventName}_${fieldName}`;
226
- fields.push({
227
- name: fieldName,
228
- tsType: jsonSchemaTypeToTs(
229
- rawProp,
230
- enums,
231
- enumNameHint,
232
- unmapped,
233
- `${eventName}.${fieldName}`,
234
- ),
235
- optional,
236
- });
237
- }
238
- } else if (entry?.metadata?.FIELD?.length) {
239
- for (const field of entry.metadata.FIELD) {
240
- fields.push({
241
- name: field.NAME,
242
- tsType: metadataJsonTypeToTs(field.JSON_TYPE, field.TYPE),
243
- optional: field.OPTIONAL !== false,
244
- });
245
- }
246
- } else {
247
- unmapped.push(`${eventName}: no INBOUND DETAILS schema or metadata.FIELD`);
248
- continue;
249
- }
250
-
251
- contracts.push({
252
- eventName,
253
- detailsInterface: eventNameToDetailsInterfaceName(eventName),
254
- fields,
255
- });
256
- }
257
-
258
- const enumDefs: MetadataEnumDef[] = [...enums.entries()]
259
- .map(([name, values]) => ({ name, values }))
260
- .sort((a, b) => a.name.localeCompare(b.name));
261
-
262
- return { contracts, enums: enumDefs, unmapped };
263
- };
264
-
265
- export const renderEventTypesFromMetadataContracts = (
266
- contracts: MetadataEventContract[],
267
- enums: MetadataEnumDef[],
268
- unmapped: string[],
269
- ): string => {
270
- const unmappedComment =
271
- unmapped.length > 0
272
- ? `/**\n * Unmapped handlers (not included in EventDetailsMap):\n${unmapped
273
- .map((line) => ` * - ${line}`)
274
- .join('\n')}\n */\n\n`
275
- : '';
276
-
277
- const enumExports = enums
278
- .map((e) => {
279
- const entries = e.values
280
- .map((value) => ` ${quoteTsString(value)}: ${quoteTsString(value)},`)
281
- .join('\n');
282
- return `export const ${e.name} = {\n${entries}\n} as const;\n\nexport type ${e.name} = (typeof ${e.name})[keyof typeof ${e.name}];`;
283
- })
284
- .join('\n\n');
285
-
286
- const interfaces = contracts
287
- .map((c) => {
288
- const lines = c.fields.map((f) => {
289
- const optional = f.optional ? '?' : '';
290
- return ` ${tsPropertyKey(f.name)}${optional}: ${f.tsType};`;
291
- });
292
- return `export interface ${c.detailsInterface} {\n${lines.join('\n')}\n}`;
293
- })
294
- .join('\n\n');
295
-
296
- const mapEntries = contracts
297
- .map((c) => ` ${tsPropertyKey(c.eventName)}: ${c.detailsInterface};`)
298
- .join('\n');
299
-
300
- const eventClasses = contracts
301
- .map((c) => {
302
- const className = eventNameToEventClassName(c.eventName);
303
- return `export class ${className} extends GenesisEvent<'${c.eventName}', ${c.detailsInterface}> {
304
- readonly MESSAGE_TYPE = '${c.eventName}' as const;
305
-
306
- constructor(details: ${c.detailsInterface}) {
307
- super(details);
308
- }
309
- }`;
310
- })
311
- .join('\n\n');
312
-
313
- const foundationCommsImport =
314
- contracts.length > 0 ? `import { GenesisEvent } from '@genesislcap/foundation-comms';\n\n` : '';
315
-
316
- return `// AUTO-GENERATED — do not edit
317
- // Source: event metadata dump (genx generate event-types --from-metadata)
318
-
319
- ${unmappedComment}${foundationCommsImport}${enumExports ? `${enumExports}\n\n` : ''}${interfaces}
320
-
321
- export interface EventDetailsMap {
322
- ${mapEntries}
323
- }
324
-
325
- export type GenesisEventName = keyof EventDetailsMap;
326
- ${eventClasses ? `\n${eventClasses}\n` : ''}`;
327
- };