@cloudcare/hvigor-ohos-plugin 0.1.1 → 0.1.2-alpha.2
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/CHANGELOG.md +15 -1
- package/README.md +53 -6
- package/hvigorCompile/ft-cold-launch-transform.ts +668 -121
- package/hvigorCompile/ft-console-log-transform.ts +90 -96
- package/hvigorCompile/ft-generated-names.ts +19 -0
- package/hvigorCompile/ft-lexical-scope.ts +65 -0
- package/hvigorCompile/ft-session-replay-transform.ts +381 -0
- package/hvigorCompile/ft-source-transform-host.ts +112 -23
- package/hvigorCompile/ft-transform-context.ts +91 -0
- package/hvigorCompile/ft-web-replay-transform.ts +61 -0
- package/index.js +5 -3
- package/package.json +12 -3
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* never changed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { hasRuntimeDependency, SourceReader, SourceDiagnosticReporter, parseApplicationSource,
|
|
11
|
+
reportSourceDiagnostic, reportSourceParseError } from './ft-transform-context';
|
|
12
|
+
import { createLexicalResolver } from './ft-lexical-scope';
|
|
13
|
+
import { allocateName, collectIdentifierNames } from './ft-generated-names';
|
|
14
|
+
|
|
10
15
|
type TypeScriptApi = any;
|
|
11
16
|
|
|
12
17
|
interface TransformCandidate {
|
|
@@ -17,11 +22,14 @@ interface TransformCandidate {
|
|
|
17
22
|
level: string;
|
|
18
23
|
source: 'console' | 'hilog';
|
|
19
24
|
callArguments: SourceRange[];
|
|
25
|
+
needsStatementSeparator: boolean;
|
|
26
|
+
generatedNames?: string[];
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
interface SourceRange {
|
|
23
30
|
start: number;
|
|
24
31
|
end: number;
|
|
32
|
+
spread: boolean;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
const HILOG_MODULES: string[] = ['@ohos.hilog', '@kit.PerformanceAnalysisKit'];
|
|
@@ -45,74 +53,53 @@ function visit(ts: TypeScriptApi, node: any, visitor: (current: any) => void): v
|
|
|
45
53
|
ts.forEachChild(node, (child: any) => visit(ts, child, visitor));
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
visit(ts, sourceFile, (node: any) => {
|
|
51
|
-
if (ts.isIdentifier(node)) {
|
|
52
|
-
identifiers.add(node.text);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
return identifiers;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isNameDeclaredOutsideImports(ts: TypeScriptApi, sourceFile: any, name: string): boolean {
|
|
59
|
-
let declared = false;
|
|
60
|
-
visit(ts, sourceFile, (node: any) => {
|
|
61
|
-
if (declared || ts.isImportDeclaration(node)) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) ||
|
|
65
|
-
ts.isParameter(node) || ts.isEnumDeclaration(node) || ts.isInterfaceDeclaration(node) ||
|
|
66
|
-
ts.isTypeAliasDeclaration(node)) {
|
|
67
|
-
const declarationName = node.name;
|
|
68
|
-
if (declarationName && ts.isIdentifier(declarationName) && declarationName.text === name) {
|
|
69
|
-
declared = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
return declared;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Set<string> {
|
|
77
|
-
const bindings = new Set<string>();
|
|
56
|
+
function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Map<string, any> {
|
|
57
|
+
const bindings = new Map<string, any>();
|
|
78
58
|
sourceFile.statements.forEach((statement: any) => {
|
|
79
59
|
if (!ts.isImportDeclaration(statement) || !statement.moduleSpecifier ||
|
|
80
60
|
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
81
|
-
!HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause) {
|
|
61
|
+
!HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause || statement.importClause.isTypeOnly) {
|
|
82
62
|
return;
|
|
83
63
|
}
|
|
84
64
|
|
|
85
65
|
const importClause = statement.importClause;
|
|
86
66
|
if (importClause.name) {
|
|
87
|
-
bindings.
|
|
67
|
+
bindings.set(importClause.name.text, importClause);
|
|
88
68
|
}
|
|
89
69
|
if (!importClause.namedBindings) {
|
|
90
70
|
return;
|
|
91
71
|
}
|
|
92
72
|
if (ts.isNamespaceImport(importClause.namedBindings)) {
|
|
93
|
-
bindings.
|
|
73
|
+
bindings.set(importClause.namedBindings.name.text, importClause.namedBindings);
|
|
94
74
|
return;
|
|
95
75
|
}
|
|
96
76
|
importClause.namedBindings.elements.forEach((element: any) => {
|
|
97
77
|
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
98
|
-
if (importedName === 'hilog') {
|
|
99
|
-
bindings.
|
|
78
|
+
if (importedName === 'hilog' && !element.isTypeOnly) {
|
|
79
|
+
bindings.set(element.name.text, element);
|
|
100
80
|
}
|
|
101
81
|
});
|
|
102
82
|
});
|
|
103
83
|
return bindings;
|
|
104
84
|
}
|
|
105
85
|
|
|
106
|
-
function
|
|
86
|
+
function needsStatementSeparator(ts: TypeScriptApi, node: any): boolean {
|
|
87
|
+
const statement = node.parent;
|
|
88
|
+
if (!ts.isExpressionStatement(statement)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const container = statement.parent;
|
|
92
|
+
// Only statement lists accept a leading empty statement. In an unbraced
|
|
93
|
+
// if/loop body it would replace the body; inside an expression it is invalid.
|
|
94
|
+
return ts.isSourceFile(container) || ts.isBlock(container) || ts.isModuleBlock(container) ||
|
|
95
|
+
ts.isCaseClause(container) || ts.isDefaultClause(container);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string,
|
|
99
|
+
unsupported: (node: any, message: string) => void): TransformCandidate[] {
|
|
107
100
|
const candidates: TransformCandidate[] = [];
|
|
108
101
|
const hiLogBindings = collectHiLogBindings(ts, sourceFile);
|
|
109
|
-
const
|
|
110
|
-
const shadowedHiLogBindings = new Set<string>();
|
|
111
|
-
hiLogBindings.forEach((binding: string) => {
|
|
112
|
-
if (isNameDeclaredOutsideImports(ts, sourceFile, binding)) {
|
|
113
|
-
shadowedHiLogBindings.add(binding);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
102
|
+
const resolve = createLexicalResolver(ts, sourceFile);
|
|
116
103
|
|
|
117
104
|
visit(ts, sourceFile, (node: any) => {
|
|
118
105
|
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) {
|
|
@@ -124,38 +111,47 @@ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): Tr
|
|
|
124
111
|
}
|
|
125
112
|
const delegateName = delegate.text;
|
|
126
113
|
const level = node.expression.name.text;
|
|
114
|
+
const isConsole = delegateName === 'console' && !resolve(delegate, delegateName) && CONSOLE_LEVELS.includes(level);
|
|
115
|
+
const isHiLog = hiLogBindings.has(delegateName) &&
|
|
116
|
+
resolve(delegate, delegateName) === hiLogBindings.get(delegateName) && HILOG_LEVELS.includes(level);
|
|
117
|
+
if (!isConsole && !isHiLog) return;
|
|
118
|
+
// Preserve short-circuit semantics instead of transforming optional calls.
|
|
119
|
+
if (ts.isOptionalChain && ts.isOptionalChain(node)) {
|
|
120
|
+
unsupported(node, 'Optional Console/HiLog calls are left unchanged and are not automatically collected.');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
127
123
|
const start = node.getStart(sourceFile);
|
|
128
124
|
const end = node.end;
|
|
129
125
|
const calleeEnd = node.expression.end;
|
|
130
126
|
if (start < 0 || end > code.length || calleeEnd < start || calleeEnd > end) {
|
|
127
|
+
unsupported(node, 'The compiler returned an invalid source range for this logging call.');
|
|
131
128
|
return;
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
const callArguments: SourceRange[] = node.arguments.map((argument: any) => ({
|
|
135
132
|
start: argument.getStart(sourceFile),
|
|
136
|
-
end: argument.end
|
|
133
|
+
end: argument.end,
|
|
134
|
+
spread: ts.isSpreadElement(argument)
|
|
137
135
|
}));
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
const statementSeparator = needsStatementSeparator(ts, node);
|
|
137
|
+
if (isConsole) {
|
|
138
|
+
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'console', callArguments,
|
|
139
|
+
needsStatementSeparator: statementSeparator });
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
if (callArguments.length >= 3 || callArguments.some(argument => argument.spread)) {
|
|
143
|
+
candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments,
|
|
144
|
+
needsStatementSeparator: statementSeparator });
|
|
145
|
+
} else {
|
|
146
|
+
unsupported(node, 'HiLog collection requires domain, tag and format arguments, or a spread argument supplying them.');
|
|
145
147
|
}
|
|
146
148
|
});
|
|
147
149
|
return candidates;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
function
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
let suffix = 1;
|
|
154
|
-
while (identifiers.has(bridgeName)) {
|
|
155
|
-
bridgeName = `${baseName}${suffix}`;
|
|
156
|
-
suffix++;
|
|
157
|
-
}
|
|
158
|
-
return bridgeName;
|
|
152
|
+
function allocateCandidateNames(candidate: TransformCandidate, identifiers: Set<string>): string[] {
|
|
153
|
+
const prefix = candidate.source === 'console' ? '__ftConsole' : '__ftHiLog';
|
|
154
|
+
return ['Call', 'Args', 'Values'].map(suffix => allocateName(identifiers, prefix + suffix));
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
function isNestedIn(candidate: TransformCandidate, parent: TransformCandidate): boolean {
|
|
@@ -187,42 +183,31 @@ function renderCandidate(code: string, candidates: TransformCandidate[], bridgeN
|
|
|
187
183
|
candidate: TransformCandidate): string {
|
|
188
184
|
const renderedArguments = candidate.callArguments.map((argument: SourceRange) =>
|
|
189
185
|
renderCandidates(code, candidates, bridgeName, argument.start, argument.end));
|
|
186
|
+
const statementSeparator = candidate.needsStatementSeparator ? ';' : '';
|
|
187
|
+
const [original, args, values] = candidate.generatedNames!;
|
|
188
|
+
const valueType = '(Object | null | undefined)';
|
|
189
|
+
const originalType = candidate.source === 'console' ? `(...${args}: ${valueType}[]) => void` :
|
|
190
|
+
`(domain: number, tag: string, format: string, ...${values}: ${valueType}[]) => void`;
|
|
191
|
+
let body: string;
|
|
190
192
|
if (candidate.source === 'console') {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
body = `${bridgeName}.console('${candidate.level}', (): void => ${original}(...${args}), ` +
|
|
194
|
+
`...(${args} as Object[]))`;
|
|
195
|
+
} else {
|
|
196
|
+
const header = `${args}[0] as number, ${args}[1] as string, ${args}[2] as string`;
|
|
197
|
+
body = `{ const ${values}: ${valueType}[] = ${args}.slice(3); ` +
|
|
198
|
+
`${bridgeName}.hilog('${candidate.level}', ${header}, (): void => ${original}(${header}, ...${values}), ` +
|
|
199
|
+
`...(${values} as Object[])); }`;
|
|
198
200
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
return `__ftHiLogArg${index - 3}`;
|
|
211
|
-
});
|
|
212
|
-
const parameterDeclarations = argumentNames.map((name: string, index: number) => {
|
|
213
|
-
if (index === 0) {
|
|
214
|
-
return `${name}: number`;
|
|
215
|
-
}
|
|
216
|
-
if (index === 1 || index === 2) {
|
|
217
|
-
return `${name}: string`;
|
|
218
|
-
}
|
|
219
|
-
return `${name}: Object`;
|
|
220
|
-
}).join(', ');
|
|
221
|
-
const originalCall = `${candidate.delegate}.${candidate.level}(${argumentNames.join(', ')})`;
|
|
222
|
-
const captureArguments = argumentNames.length > 3 ? `, ${argumentNames.slice(3).join(', ')}` : '';
|
|
223
|
-
return `((${parameterDeclarations}): void => ${bridgeName}.hilog('${candidate.level}', ` +
|
|
224
|
-
`${argumentNames[0]}, ${argumentNames[1]}, ${argumentNames[2]}, (): void => ${originalCall}` +
|
|
225
|
-
`${captureArguments}))(${renderedArguments.join(', ')})`;
|
|
201
|
+
// Resolve and bind the original method before evaluating its arguments.
|
|
202
|
+
// Casting only the capture array keeps compatibility with older runtime HARs;
|
|
203
|
+
// null/undefined values themselves are forwarded unchanged.
|
|
204
|
+
// HiLog is a platform namespace, not an ArkTS object receiver. Its exported
|
|
205
|
+
// functions are static; console methods retain their receiver through bind.
|
|
206
|
+
const callee = `${candidate.delegate}.${candidate.level}`;
|
|
207
|
+
const captured = candidate.source === 'console' ? `${callee}.bind(${candidate.delegate})` : callee;
|
|
208
|
+
const argumentsText = renderedArguments.length > 0 ? ', ' + renderedArguments.join(', ') : '';
|
|
209
|
+
return `${statementSeparator}((${original}: ${originalType}, ...${args}: ${valueType}[]): void => ${body})` +
|
|
210
|
+
`(${captured} as ${originalType}${argumentsText})`;
|
|
226
211
|
}
|
|
227
212
|
|
|
228
213
|
function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
@@ -235,17 +220,26 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
|
235
220
|
return offset;
|
|
236
221
|
}
|
|
237
222
|
|
|
238
|
-
export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string
|
|
239
|
-
|
|
223
|
+
export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader,
|
|
224
|
+
reportDiagnostic?: SourceDiagnosticReporter): string {
|
|
225
|
+
if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER) ||
|
|
226
|
+
!hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource)) {
|
|
240
227
|
return code;
|
|
241
228
|
}
|
|
242
229
|
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
243
|
-
const
|
|
230
|
+
const unsupported: Array<{ node: any; message: string }> = [];
|
|
231
|
+
const candidates = collectCandidates(ts, sourceFile, code, (node, message) => unsupported.push({ node, message }));
|
|
232
|
+
if (candidates.length === 0 && unsupported.length === 0) return code;
|
|
233
|
+
if (reportSourceParseError(ts, parseApplicationSource(ts, id, code), reportDiagnostic)) return code;
|
|
234
|
+
unsupported.forEach(item => reportSourceDiagnostic(sourceFile, item.node.getStart(sourceFile),
|
|
235
|
+
'unsupported-log-call', item.message, reportDiagnostic));
|
|
244
236
|
if (candidates.length === 0) {
|
|
245
237
|
return code;
|
|
246
238
|
}
|
|
247
239
|
|
|
248
|
-
const
|
|
240
|
+
const identifiers = collectIdentifierNames(ts, sourceFile);
|
|
241
|
+
const bridgeName = allocateName(identifiers, '__ftConsoleLogBridge');
|
|
242
|
+
candidates.forEach(candidate => { candidate.generatedNames = allocateCandidateNames(candidate, identifiers); });
|
|
249
243
|
const transformedCode = renderCandidates(code, candidates, bridgeName, 0, code.length);
|
|
250
244
|
const insertionOffset = findImportInsertionOffset(ts, sourceFile);
|
|
251
245
|
const transformedInsertionOffset = renderCandidates(code, candidates, bridgeName, 0, insertionOffset).length;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Reserve generated identifiers across all scopes and every nested rewrite. */
|
|
2
|
+
export function allocateName(identifiers: Set<string>, base: string): string {
|
|
3
|
+
let candidate = base;
|
|
4
|
+
let suffix = 1;
|
|
5
|
+
while (identifiers.has(candidate)) candidate = `${base}${suffix++}`;
|
|
6
|
+
identifiers.add(candidate);
|
|
7
|
+
return candidate;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function collectIdentifierNames(ts: any, source: any): Set<string> {
|
|
11
|
+
const identifiers = new Set<string>();
|
|
12
|
+
const visit = (node: any): void => {
|
|
13
|
+
if (ts.isIdentifier(node)) identifiers.add(node.text);
|
|
14
|
+
if (node.name && ts.isStringLiteral(node.name)) identifiers.add(node.name.text);
|
|
15
|
+
ts.forEachChild(node, visit);
|
|
16
|
+
};
|
|
17
|
+
visit(source);
|
|
18
|
+
return identifiers;
|
|
19
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Value bindings visible at a call site, including declarations later in the same scope. */
|
|
2
|
+
interface Scope {
|
|
3
|
+
parent?: Scope;
|
|
4
|
+
functionScope: boolean;
|
|
5
|
+
bindings: Map<string, any>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createLexicalResolver(ts: any, source: any): (location: any, name: string) => any | undefined {
|
|
9
|
+
const scopes = new WeakMap<object, Scope>();
|
|
10
|
+
const root: Scope = { functionScope: true, bindings: new Map() };
|
|
11
|
+
const bind = (scope: Scope, name: any, declaration: any): void => {
|
|
12
|
+
if (!name) return;
|
|
13
|
+
if (ts.isIdentifier(name)) scope.bindings.set(name.text, declaration);
|
|
14
|
+
else if (name.elements) name.elements.forEach((element: any) => {
|
|
15
|
+
if (ts.isBindingElement(element)) bind(scope, element.name, declaration);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
const walk = (node: any, outer: Scope): void => {
|
|
19
|
+
const struct = typeof ts.isStructDeclaration === 'function' && ts.isStructDeclaration(node);
|
|
20
|
+
const classLike = ts.isClassDeclaration(node) || ts.isClassExpression(node) || struct;
|
|
21
|
+
if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || struct || ts.isEnumDeclaration(node) ||
|
|
22
|
+
ts.isModuleDeclaration(node)) && node.name) {
|
|
23
|
+
bind(outer, node.name, node);
|
|
24
|
+
}
|
|
25
|
+
let scope = outer;
|
|
26
|
+
const functionLike = ts.isFunctionLike(node);
|
|
27
|
+
if (functionLike || ts.isBlock(node) || ts.isCaseBlock(node) || ts.isCatchClause(node) ||
|
|
28
|
+
ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) ||
|
|
29
|
+
ts.isModuleBlock(node) || classLike) {
|
|
30
|
+
scope = { parent: outer, functionScope: functionLike || ts.isModuleBlock(node), bindings: new Map() };
|
|
31
|
+
}
|
|
32
|
+
scopes.set(node, scope);
|
|
33
|
+
if (functionLike) {
|
|
34
|
+
if (ts.isFunctionExpression(node) && node.name) bind(scope, node.name, node);
|
|
35
|
+
node.parameters.forEach((parameter: any) => bind(scope, parameter.name, parameter));
|
|
36
|
+
}
|
|
37
|
+
if (classLike && node.name) bind(scope, node.name, node);
|
|
38
|
+
if (ts.isVariableDeclaration(node)) {
|
|
39
|
+
const list = node.parent;
|
|
40
|
+
let target = scope;
|
|
41
|
+
if (ts.isVariableDeclarationList(list) && !(list.flags & ts.NodeFlags.BlockScoped)) {
|
|
42
|
+
while (!target.functionScope && target.parent) target = target.parent;
|
|
43
|
+
}
|
|
44
|
+
bind(target, node.name, node);
|
|
45
|
+
}
|
|
46
|
+
if (ts.isImportDeclaration(node) && node.importClause && !node.importClause.isTypeOnly) {
|
|
47
|
+
const clause = node.importClause;
|
|
48
|
+
if (clause.name) bind(scope, clause.name, clause);
|
|
49
|
+
const bindings = clause.namedBindings;
|
|
50
|
+
if (bindings && ts.isNamespaceImport(bindings)) bind(scope, bindings.name, bindings);
|
|
51
|
+
else if (bindings) bindings.elements.forEach((element: any) => {
|
|
52
|
+
if (!element.isTypeOnly) bind(scope, element.name, element);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (ts.isImportEqualsDeclaration(node) && !node.isTypeOnly) bind(scope, node.name, node);
|
|
56
|
+
ts.forEachChild(node, (child: any) => walk(child, scope));
|
|
57
|
+
};
|
|
58
|
+
walk(source, root);
|
|
59
|
+
return (location: any, name: string): any | undefined => {
|
|
60
|
+
for (let scope = scopes.get(location); scope; scope = scope.parent) {
|
|
61
|
+
if (scope.bindings.has(name)) return scope.bindings.get(name);
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
}
|