devlensio 0.4.2 → 0.4.4
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 +6 -4
- package/dist/filesystem/index.js +6 -0
- package/dist/filesystem/index.test.js +170 -0
- package/dist/filesystem/reactRouterRoutes.d.ts +2 -0
- package/dist/filesystem/reactRouterRoutes.js +243 -0
- package/dist/graph/edges/apiFetchEdges.js +68 -137
- package/dist/graph/edges/helpers/routeMatching.d.ts +8 -0
- package/dist/graph/edges/helpers/routeMatching.js +122 -0
- package/dist/graph/edges/navigationEdges.d.ts +5 -0
- package/dist/graph/edges/navigationEdges.js +339 -0
- package/dist/graph/edges/routeEdge.js +25 -0
- package/dist/graph/index.js +12 -1
- package/dist/graph/index.test.js +263 -8
- package/dist/parser/extractors/functions.d.ts +5 -0
- package/dist/parser/extractors/functions.js +5 -5
- package/dist/parser/extractors/objectMethods.d.ts +4 -0
- package/dist/parser/extractors/objectMethods.js +141 -0
- package/dist/parser/index.js +3 -1
- package/dist/parser/index.test.js +129 -9
- package/dist/pipeline/index.js +26 -0
- package/dist/types.d.ts +3 -2
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// This file detects the backend routes of the NEXTjs only
|
|
2
2
|
import { Project, SyntaxKind } from "ts-morph";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { extractUrlFromArg, normalizeUrl, urlPathtoRegex } from "./helpers/routeMatching.js";
|
|
4
5
|
const CALLER_CONFIG = {
|
|
5
6
|
"fetch": { inferMethod: "from-options", defaultMethod: "GET" },
|
|
6
7
|
"axios.get": { inferMethod: "fixed", method: "GET" },
|
|
@@ -20,16 +21,6 @@ const CALLER_CONFIG = {
|
|
|
20
21
|
"useSWR": { inferMethod: "fixed", method: "GET" },
|
|
21
22
|
"useSWRMutation": { inferMethod: "fixed", method: "UNKNOWN" },
|
|
22
23
|
};
|
|
23
|
-
function urlPathtoRegex(urlPath) {
|
|
24
|
-
const pattern = urlPath.split("/").map(segment => {
|
|
25
|
-
if (segment.startsWith(":") && segment.endsWith("*"))
|
|
26
|
-
return ".+"; // catch-all :slug*
|
|
27
|
-
if (segment.startsWith(":"))
|
|
28
|
-
return "[^/]+"; // dynamic :id
|
|
29
|
-
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // escape static segments
|
|
30
|
-
}).join("\\/");
|
|
31
|
-
return new RegExp(`^${pattern}$`);
|
|
32
|
-
}
|
|
33
24
|
// Groups Next.js API ROUTE nodes by HTTP method for efficient lookup.
|
|
34
25
|
// Each route node carries a concrete method, so it is indexed under that one
|
|
35
26
|
// method key. Callers with an UNKNOWN method (e.g. useSWRMutation) are handled
|
|
@@ -54,97 +45,6 @@ function buildRouteIndex(nodes) {
|
|
|
54
45
|
}
|
|
55
46
|
return index;
|
|
56
47
|
}
|
|
57
|
-
/*
|
|
58
|
-
Extracts string value from an initializer node.
|
|
59
|
-
Handles 3 cases:
|
|
60
|
-
'string literal' → returns value directly
|
|
61
|
-
`no-substitution template` → returns value directly
|
|
62
|
-
`template ${expr} literal` → preserves ${...} as-is so normalizeUrl handles it later
|
|
63
|
-
*/
|
|
64
|
-
function resolveVariableValue(initializer) {
|
|
65
|
-
const kind = initializer.getKind();
|
|
66
|
-
if (kind === SyntaxKind.StringLiteral) {
|
|
67
|
-
return initializer.getLiteralText();
|
|
68
|
-
}
|
|
69
|
-
if (kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
70
|
-
return initializer.getLiteralText();
|
|
71
|
-
}
|
|
72
|
-
if (kind === SyntaxKind.TemplateExpression) {
|
|
73
|
-
const head = initializer.getHead().getLiteralText();
|
|
74
|
-
const spans = initializer.getTemplateSpans().map((span) => `\${${span.getExpression().getText()}}${span.getLiteral().getLiteralText()}`);
|
|
75
|
-
return head + spans.join("");
|
|
76
|
-
}
|
|
77
|
-
return null; // object, function, computed — can't resolve
|
|
78
|
-
}
|
|
79
|
-
/*
|
|
80
|
-
Resolves a variable name to its string value.
|
|
81
|
-
Strategy:
|
|
82
|
-
1. Look in the same file
|
|
83
|
-
2. Walk named imports → find the source file → look there
|
|
84
|
-
Files not yet in the project are added lazily so we don't pre-load everything.
|
|
85
|
-
*/
|
|
86
|
-
function resolveUrlVariable(varName, sourceFile, project) {
|
|
87
|
-
//case 1 -> Variable exists in same file
|
|
88
|
-
const localDecl = sourceFile.getVariableDeclaration(varName);
|
|
89
|
-
if (localDecl) {
|
|
90
|
-
const init = localDecl.getInitializer();
|
|
91
|
-
if (init)
|
|
92
|
-
return resolveVariableValue(init);
|
|
93
|
-
}
|
|
94
|
-
//case 2 -> named imports
|
|
95
|
-
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
96
|
-
const match = importDecl.getNamedImports().find(n => n.getName() === varName);
|
|
97
|
-
if (!match)
|
|
98
|
-
continue;
|
|
99
|
-
// Try to get the source file — it may not be in the project yet
|
|
100
|
-
let importedFile = importDecl.getModuleSpecifierSourceFile();
|
|
101
|
-
if (!importedFile) {
|
|
102
|
-
// Lazily add the file to the project
|
|
103
|
-
const specifier = importDecl.getModuleSpecifierValue();
|
|
104
|
-
const currentDir = path.dirname(sourceFile.getFilePath());
|
|
105
|
-
const base = path.resolve(currentDir, specifier);
|
|
106
|
-
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
107
|
-
try {
|
|
108
|
-
importedFile = project.addSourceFileAtPath(base + ext);
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (!importedFile)
|
|
117
|
-
continue;
|
|
118
|
-
const importedDecl = importedFile.getVariableDeclaration(varName);
|
|
119
|
-
if (importedDecl) {
|
|
120
|
-
const init = importedDecl.getInitializer();
|
|
121
|
-
if (init)
|
|
122
|
-
return resolveVariableValue(init);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
// URL Normalization
|
|
128
|
-
// Converts a raw URL string from the call site into a normalized form
|
|
129
|
-
// that can be compared against route index entries.
|
|
130
|
-
// Examples:
|
|
131
|
-
// `/api/users/${id}` → /api/users/:dynamic
|
|
132
|
-
// `/api/users/${org}/${id}` → /api/users/:dynamic/:dynamic
|
|
133
|
-
// /api/users?foo=bar → /api/users
|
|
134
|
-
// /api/users/ → /api/users
|
|
135
|
-
function normalizeUrl(rawUrl) {
|
|
136
|
-
// Skip external URLs
|
|
137
|
-
if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://"))
|
|
138
|
-
return null;
|
|
139
|
-
// Must start with /
|
|
140
|
-
if (!rawUrl.startsWith("/"))
|
|
141
|
-
return null;
|
|
142
|
-
return rawUrl
|
|
143
|
-
.split("?")[0] // strip query string
|
|
144
|
-
.replace(/\$\{[^}]+\}/g, ":dynamic") // ${anything} → :dynamic
|
|
145
|
-
.replace(/\/+$/, "") // strip trailing slash
|
|
146
|
-
|| "/"; // fallback to root if empty
|
|
147
|
-
}
|
|
148
48
|
// Route Matching
|
|
149
49
|
// Matches a normalized URL + HTTP method against the route index.
|
|
150
50
|
// Rules:
|
|
@@ -167,46 +67,19 @@ function matchRouteEntries(normalizedUrl, method, routeIndex) {
|
|
|
167
67
|
// e.g. "/api/users/:dynamic" matches the regex for route "/api/users/:id".
|
|
168
68
|
return candidates.filter(e => e.isDynamic && e.urlRegex.test(normalizedUrl));
|
|
169
69
|
}
|
|
170
|
-
// URL Argument Extraction
|
|
171
|
-
// Shared helper — reconstructs a template literal preserving ${...} as-is
|
|
172
|
-
// so normalizeUrl can replace them with :dynamic later.
|
|
173
|
-
// Same logic as resolveVariableValue's TemplateExpression branch.
|
|
174
|
-
function reconstructTemplate(templateExpr) {
|
|
175
|
-
const head = templateExpr.getHead().getLiteralText();
|
|
176
|
-
const spans = templateExpr.getTemplateSpans().map((span) => `\${${span.getExpression().getText()}}${span.getLiteral().getLiteralText()}`);
|
|
177
|
-
return head + spans.join("");
|
|
178
|
-
}
|
|
179
|
-
// Extracts a URL string from a call argument.
|
|
180
|
-
// Handles 4 cases:
|
|
181
|
-
// '/api/users' → string literal → return directly
|
|
182
|
-
// `/api/users` → no-sub template → return directly
|
|
183
|
-
// `/api/users/${id}` → template expression → preserve ${...}
|
|
184
|
-
// API_URL → identifier → resolve via resolveUrlVariable
|
|
185
|
-
// Returns null if the arg is an object / array / expression we can't resolve.
|
|
186
|
-
function extractUrlFromArg(arg, sourceFile, project) {
|
|
187
|
-
const kind = arg.getKind();
|
|
188
|
-
if (kind === SyntaxKind.StringLiteral ||
|
|
189
|
-
kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
190
|
-
const url = arg.getLiteralText();
|
|
191
|
-
return { rawUrl: url, resolvedUrl: url };
|
|
192
|
-
}
|
|
193
|
-
if (kind === SyntaxKind.TemplateExpression) {
|
|
194
|
-
const url = reconstructTemplate(arg);
|
|
195
|
-
return { rawUrl: url, resolvedUrl: url };
|
|
196
|
-
}
|
|
197
|
-
if (kind === SyntaxKind.Identifier) {
|
|
198
|
-
const varName = arg.getText();
|
|
199
|
-
const resolved = resolveUrlVariable(varName, sourceFile, project);
|
|
200
|
-
if (!resolved)
|
|
201
|
-
return null;
|
|
202
|
-
return { rawUrl: varName, resolvedUrl: resolved };
|
|
203
|
-
}
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
70
|
// Scans EVERY call expression in a source file exactly once.
|
|
207
71
|
// For each call matching CALLER_CONFIG:
|
|
208
72
|
// - infers the HTTP method
|
|
209
73
|
// - extracts and resolves the URL argument
|
|
74
|
+
// It additionally scans for backend routes used as a resource URL:
|
|
75
|
+
// - JSX src/href attributes — tag-agnostic, so it covers native elements
|
|
76
|
+
// (<video src="/api/..">, <iframe src={url}>, <a href={`/api/${id}`}>)
|
|
77
|
+
// AND third-party player components (<HlsPlayer src={url} />,
|
|
78
|
+
// <ReactHlsPlayer src={...} />).
|
|
79
|
+
// - object-literal src/href properties — config-object style usage where a
|
|
80
|
+
// route is handed to a third-party player/function, e.g.
|
|
81
|
+
// loadPlayer({ src: '/api/stream' }).
|
|
82
|
+
// The browser issues a GET for any such resource, so these are emitted as GET.
|
|
210
83
|
// The expensive AST traversal happens once per file here; callers then
|
|
211
84
|
// attribute the resulting calls to nodes by line range (see detectNextjsApiCallEdges).
|
|
212
85
|
// Note: useQuery/useMutation inner fetch/axios calls are captured naturally
|
|
@@ -270,6 +143,64 @@ function extractApiCallsFromFile(sourceFile, project) {
|
|
|
270
143
|
},
|
|
271
144
|
});
|
|
272
145
|
}
|
|
146
|
+
// JSX src/href attributes — tag-agnostic, so native elements (video, iframe,
|
|
147
|
+
// a) and third-party player components (HlsPlayer, ReactHlsPlayer, …) are all
|
|
148
|
+
// covered. The browser fetches the resource with GET.
|
|
149
|
+
for (const attr of sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
|
|
150
|
+
const attrName = attr.getNameNode().getText();
|
|
151
|
+
if (attrName !== "src" && attrName !== "href")
|
|
152
|
+
continue;
|
|
153
|
+
const initializer = attr.getInitializer();
|
|
154
|
+
if (!initializer)
|
|
155
|
+
continue;
|
|
156
|
+
// value is either a bare string (src="/api/x") or a JSX expression
|
|
157
|
+
// container (src={url} / src={`/api/${id}`})
|
|
158
|
+
let valueNode;
|
|
159
|
+
const initKind = initializer.getKind();
|
|
160
|
+
if (initKind === SyntaxKind.StringLiteral) {
|
|
161
|
+
valueNode = initializer;
|
|
162
|
+
}
|
|
163
|
+
else if (initKind === SyntaxKind.JsxExpression) {
|
|
164
|
+
valueNode = initializer.getExpression();
|
|
165
|
+
}
|
|
166
|
+
if (!valueNode)
|
|
167
|
+
continue;
|
|
168
|
+
const urlResult = extractUrlFromArg(valueNode, sourceFile, project);
|
|
169
|
+
if (!urlResult)
|
|
170
|
+
continue;
|
|
171
|
+
results.push({
|
|
172
|
+
startLine: attr.getStartLineNumber(),
|
|
173
|
+
call: {
|
|
174
|
+
callerType: `jsx-${attrName}`,
|
|
175
|
+
rawUrl: urlResult.rawUrl,
|
|
176
|
+
resolvedUrl: urlResult.resolvedUrl,
|
|
177
|
+
method: "GET",
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// Object-literal src/href properties — config-object style usage where a
|
|
182
|
+
// backend route is passed to a third-party player/function, e.g.
|
|
183
|
+
// loadPlayer({ src: '/api/stream' }). Same GET semantics.
|
|
184
|
+
for (const prop of sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
185
|
+
const name = prop.getName();
|
|
186
|
+
if (name !== "src" && name !== "href")
|
|
187
|
+
continue;
|
|
188
|
+
const init = prop.getInitializer();
|
|
189
|
+
if (!init)
|
|
190
|
+
continue;
|
|
191
|
+
const urlResult = extractUrlFromArg(init, sourceFile, project);
|
|
192
|
+
if (!urlResult)
|
|
193
|
+
continue;
|
|
194
|
+
results.push({
|
|
195
|
+
startLine: prop.getStartLineNumber(),
|
|
196
|
+
call: {
|
|
197
|
+
callerType: `prop-${name}`,
|
|
198
|
+
rawUrl: urlResult.rawUrl,
|
|
199
|
+
resolvedUrl: urlResult.resolvedUrl,
|
|
200
|
+
method: "GET",
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
273
204
|
return results;
|
|
274
205
|
}
|
|
275
206
|
//MAIN FUNCTION
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Project, SourceFile } from "ts-morph";
|
|
2
|
+
export declare function urlPathtoRegex(urlPath: string): RegExp;
|
|
3
|
+
export declare function normalizeUrl(rawUrl: string): string | null;
|
|
4
|
+
export declare function reconstructTemplate(templateExpr: any): string;
|
|
5
|
+
export declare function extractUrlFromArg(arg: any, sourceFile: SourceFile, project: Project): {
|
|
6
|
+
rawUrl: string;
|
|
7
|
+
resolvedUrl: string;
|
|
8
|
+
} | null;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Shared route/URL matching helpers used by both the Next.js API-call detector
|
|
2
|
+
// (apiFetchEdges.ts) and the navigation detector (navigationEdges.ts).
|
|
3
|
+
import { SyntaxKind } from "ts-morph";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
// Converts a route urlPath into a matching RegExp.
|
|
6
|
+
// :id → [^/]+ (dynamic segment)
|
|
7
|
+
// :slug* → .+ (catch-all)
|
|
8
|
+
// static → escaped literally
|
|
9
|
+
export function urlPathtoRegex(urlPath) {
|
|
10
|
+
const pattern = urlPath.split("/").map(segment => {
|
|
11
|
+
if (segment.startsWith(":") && segment.endsWith("*"))
|
|
12
|
+
return ".+"; // catch-all :slug*
|
|
13
|
+
if (segment.startsWith(":"))
|
|
14
|
+
return "[^/]+"; // dynamic :id
|
|
15
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // escape static segments
|
|
16
|
+
}).join("\\/");
|
|
17
|
+
return new RegExp(`^${pattern}$`);
|
|
18
|
+
}
|
|
19
|
+
// Converts a raw URL string from a call site into a normalized form comparable
|
|
20
|
+
// against route index entries.
|
|
21
|
+
// `/api/users/${id}` → /api/users/:dynamic
|
|
22
|
+
// /api/users?foo=bar → /api/users
|
|
23
|
+
// /api/users/ → /api/users
|
|
24
|
+
// Returns null for external (http/https) or non-"/"-prefixed URLs.
|
|
25
|
+
export function normalizeUrl(rawUrl) {
|
|
26
|
+
if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://"))
|
|
27
|
+
return null;
|
|
28
|
+
if (!rawUrl.startsWith("/"))
|
|
29
|
+
return null;
|
|
30
|
+
return rawUrl
|
|
31
|
+
.split("?")[0] // strip query string
|
|
32
|
+
.replace(/\$\{[^}]+\}/g, ":dynamic") // ${anything} → :dynamic
|
|
33
|
+
.replace(/\/+$/, "") // strip trailing slash
|
|
34
|
+
|| "/"; // fallback to root if empty
|
|
35
|
+
}
|
|
36
|
+
// Reconstructs a template literal preserving ${...} as-is so normalizeUrl can
|
|
37
|
+
// replace them with :dynamic later.
|
|
38
|
+
export function reconstructTemplate(templateExpr) {
|
|
39
|
+
const head = templateExpr.getHead().getLiteralText();
|
|
40
|
+
const spans = templateExpr.getTemplateSpans().map((span) => `\${${span.getExpression().getText()}}${span.getLiteral().getLiteralText()}`);
|
|
41
|
+
return head + spans.join("");
|
|
42
|
+
}
|
|
43
|
+
// Extracts a string value from an initializer node (literal / template / etc).
|
|
44
|
+
function resolveVariableValue(initializer) {
|
|
45
|
+
const kind = initializer.getKind();
|
|
46
|
+
if (kind === SyntaxKind.StringLiteral) {
|
|
47
|
+
return initializer.getLiteralText();
|
|
48
|
+
}
|
|
49
|
+
if (kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
50
|
+
return initializer.getLiteralText();
|
|
51
|
+
}
|
|
52
|
+
if (kind === SyntaxKind.TemplateExpression) {
|
|
53
|
+
const head = initializer.getHead().getLiteralText();
|
|
54
|
+
const spans = initializer.getTemplateSpans().map((span) => `\${${span.getExpression().getText()}}${span.getLiteral().getLiteralText()}`);
|
|
55
|
+
return head + spans.join("");
|
|
56
|
+
}
|
|
57
|
+
return null; // object, function, computed — can't resolve
|
|
58
|
+
}
|
|
59
|
+
// Resolves a variable name to its string value (same file first, then named imports).
|
|
60
|
+
function resolveUrlVariable(varName, sourceFile, project) {
|
|
61
|
+
const localDecl = sourceFile.getVariableDeclaration(varName);
|
|
62
|
+
if (localDecl) {
|
|
63
|
+
const init = localDecl.getInitializer();
|
|
64
|
+
if (init)
|
|
65
|
+
return resolveVariableValue(init);
|
|
66
|
+
}
|
|
67
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
68
|
+
const match = importDecl.getNamedImports().find(n => n.getName() === varName);
|
|
69
|
+
if (!match)
|
|
70
|
+
continue;
|
|
71
|
+
let importedFile = importDecl.getModuleSpecifierSourceFile();
|
|
72
|
+
if (!importedFile) {
|
|
73
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
74
|
+
const currentDir = path.dirname(sourceFile.getFilePath());
|
|
75
|
+
const base = path.resolve(currentDir, specifier);
|
|
76
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
77
|
+
try {
|
|
78
|
+
importedFile = project.addSourceFileAtPath(base + ext);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!importedFile)
|
|
87
|
+
continue;
|
|
88
|
+
const importedDecl = importedFile.getVariableDeclaration(varName);
|
|
89
|
+
if (importedDecl) {
|
|
90
|
+
const init = importedDecl.getInitializer();
|
|
91
|
+
if (init)
|
|
92
|
+
return resolveVariableValue(init);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// Extracts a URL string from a call/attr argument node.
|
|
98
|
+
// '/api/users' → string literal → return directly
|
|
99
|
+
// `/api/users` → no-sub template → return directly
|
|
100
|
+
// `/api/users/${id}` → template expression → preserve ${...}
|
|
101
|
+
// API_URL → identifier → resolve via resolveUrlVariable
|
|
102
|
+
// Returns null for objects/arrays/expressions we can't resolve.
|
|
103
|
+
export function extractUrlFromArg(arg, sourceFile, project) {
|
|
104
|
+
const kind = arg.getKind();
|
|
105
|
+
if (kind === SyntaxKind.StringLiteral ||
|
|
106
|
+
kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
107
|
+
const url = arg.getLiteralText();
|
|
108
|
+
return { rawUrl: url, resolvedUrl: url };
|
|
109
|
+
}
|
|
110
|
+
if (kind === SyntaxKind.TemplateExpression) {
|
|
111
|
+
const url = reconstructTemplate(arg);
|
|
112
|
+
return { rawUrl: url, resolvedUrl: url };
|
|
113
|
+
}
|
|
114
|
+
if (kind === SyntaxKind.Identifier) {
|
|
115
|
+
const varName = arg.getText();
|
|
116
|
+
const resolved = resolveUrlVariable(varName, sourceFile, project);
|
|
117
|
+
if (!resolved)
|
|
118
|
+
return null;
|
|
119
|
+
return { rawUrl: varName, resolvedUrl: resolved };
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|