@nestia/core 12.0.0-dev.20260601.1 → 12.0.0-dev.20260612.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/LICENSE +21 -21
- package/MIGRATION.md +169 -169
- package/README.md +93 -93
- package/lib/adaptors/McpAdaptor.d.ts +75 -0
- package/lib/adaptors/McpAdaptor.js +257 -0
- package/lib/adaptors/McpAdaptor.js.map +1 -0
- package/lib/adaptors/WebSocketAdaptor.js +4 -4
- package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
- package/lib/decorators/McpRoute.d.ts +69 -0
- package/lib/decorators/McpRoute.js +58 -0
- package/lib/decorators/McpRoute.js.map +1 -0
- package/lib/decorators/TypedParam.js +4 -4
- package/lib/decorators/TypedParam.js.map +1 -1
- package/lib/decorators/TypedRoute.js +1 -1
- package/lib/decorators/TypedRoute.js.map +1 -1
- package/lib/decorators/internal/IMcpRouteReflect.d.ts +2 -0
- package/lib/decorators/internal/IMcpRouteReflect.js +3 -0
- package/lib/decorators/internal/IMcpRouteReflect.js.map +1 -0
- package/lib/decorators/internal/get_path_and_querify.js +4 -4
- package/lib/decorators/internal/get_path_and_querify.js.map +1 -1
- package/lib/decorators/internal/get_path_and_stringify.js +4 -4
- package/lib/decorators/internal/get_path_and_stringify.js.map +1 -1
- package/lib/decorators/internal/load_controller.js +34 -65
- package/lib/decorators/internal/load_controller.js.map +1 -1
- package/lib/decorators/internal/validate_request_body.js +4 -4
- package/lib/decorators/internal/validate_request_body.js.map +1 -1
- package/lib/decorators/internal/validate_request_form_data.js +4 -4
- package/lib/decorators/internal/validate_request_form_data.js.map +1 -1
- package/lib/decorators/internal/validate_request_headers.js +4 -4
- package/lib/decorators/internal/validate_request_headers.js.map +1 -1
- package/lib/decorators/internal/validate_request_query.js +4 -4
- package/lib/decorators/internal/validate_request_query.js.map +1 -1
- package/lib/module.d.ts +2 -0
- package/lib/module.js +2 -0
- package/lib/module.js.map +1 -1
- package/native/cmd/ttsc-nestia/main.go +11 -11
- package/native/go.mod +32 -32
- package/native/go.sum +54 -54
- package/native/plugin/plan.go +102 -102
- package/native/transform/ast.go +32 -32
- package/native/transform/build.go +380 -444
- package/native/transform/cleanup.go +408 -408
- package/native/transform/contributor.go +97 -68
- package/native/transform/core_querify.go +231 -227
- package/native/transform/core_transform.go +1996 -1713
- package/native/transform/core_websocket.go +115 -115
- package/native/transform/exports.go +13 -13
- package/native/transform/mcp_transform.go +414 -0
- package/native/transform/node_transform.go +357 -0
- package/native/transform/path_rewrite.go +285 -285
- package/native/transform/printer.go +244 -244
- package/native/transform/rewrite.go +668 -662
- package/native/transform/run.go +73 -73
- package/native/transform/transform.go +336 -403
- package/native/transform/typia_fast.go +352 -326
- package/native/transform/typia_replacement.go +24 -24
- package/native/transform.cjs +43 -43
- package/package.json +15 -8
- package/src/adaptors/McpAdaptor.ts +276 -0
- package/src/adaptors/WebSocketAdaptor.ts +429 -429
- package/src/decorators/DynamicModule.ts +44 -44
- package/src/decorators/EncryptedBody.ts +97 -97
- package/src/decorators/EncryptedController.ts +40 -40
- package/src/decorators/EncryptedModule.ts +98 -98
- package/src/decorators/EncryptedRoute.ts +213 -213
- package/src/decorators/HumanRoute.ts +21 -21
- package/src/decorators/McpRoute.ts +154 -0
- package/src/decorators/NoTransformConfigurationError.ts +40 -40
- package/src/decorators/PlainBody.ts +76 -76
- package/src/decorators/SwaggerCustomizer.ts +97 -97
- package/src/decorators/SwaggerExample.ts +180 -180
- package/src/decorators/TypedBody.ts +57 -57
- package/src/decorators/TypedException.ts +147 -147
- package/src/decorators/TypedFormData.ts +187 -187
- package/src/decorators/TypedHeaders.ts +66 -66
- package/src/decorators/TypedParam.ts +77 -77
- package/src/decorators/TypedQuery.ts +234 -234
- package/src/decorators/TypedRoute.ts +198 -196
- package/src/decorators/WebSocketRoute.ts +242 -242
- package/src/decorators/doNotThrowTransformError.ts +5 -5
- package/src/decorators/internal/EncryptedConstant.ts +2 -2
- package/src/decorators/internal/IMcpRouteReflect.ts +40 -0
- package/src/decorators/internal/IWebSocketRouteReflect.ts +23 -23
- package/src/decorators/internal/get_path_and_querify.ts +94 -94
- package/src/decorators/internal/get_path_and_stringify.ts +110 -110
- package/src/decorators/internal/get_text_body.ts +16 -16
- package/src/decorators/internal/headers_to_object.ts +11 -11
- package/src/decorators/internal/is_request_body_undefined.ts +12 -12
- package/src/decorators/internal/load_controller.ts +91 -76
- package/src/decorators/internal/route_error.ts +43 -43
- package/src/decorators/internal/validate_request_body.ts +64 -64
- package/src/decorators/internal/validate_request_form_data.ts +67 -67
- package/src/decorators/internal/validate_request_headers.ts +76 -76
- package/src/decorators/internal/validate_request_query.ts +83 -83
- package/src/index.ts +5 -5
- package/src/module.ts +25 -23
- package/src/options/IRequestBodyValidator.ts +20 -20
- package/src/options/IRequestFormDataProps.ts +27 -27
- package/src/options/IRequestHeadersValidator.ts +22 -22
- package/src/options/IRequestQueryValidator.ts +20 -20
- package/src/options/IResponseBodyQuerifier.ts +25 -25
- package/src/options/IResponseBodyStringifier.ts +30 -30
- package/src/transform.ts +101 -101
- package/src/typings/Creator.ts +3 -3
- package/src/typings/get-function-location.d.ts +7 -7
- package/src/utils/ArrayUtil.ts +7 -7
- package/src/utils/ExceptionManager.ts +115 -115
- package/src/utils/Singleton.ts +16 -16
- package/src/utils/SourceFinder.ts +54 -54
- package/src/utils/VersioningStrategy.ts +27 -27
- package/native/transform/cleanup_test.go +0 -76
- package/native/transform/commonjs_import_alias_test.go +0 -49
- package/native/transform/core_dispatch_test.go +0 -127
- package/native/transform/path_rewrite_test.go +0 -243
- package/native/transform/rewrite_test.go +0 -118
- package/native/transform/rewrite_unique_base_test.go +0 -48
|
@@ -1,408 +1,408 @@
|
|
|
1
|
-
package transform
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"fmt"
|
|
5
|
-
"sort"
|
|
6
|
-
"strings"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
func cleanupTransformedText(text string) string {
|
|
10
|
-
return cleanupTransformedTextWithRuntimeAliases(text, nil)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
func cleanupTransformedTextWithRuntimeAliases(text string, aliases []string) string {
|
|
14
|
-
text = removeUnusedTypiaImports(text)
|
|
15
|
-
if aliases == nil {
|
|
16
|
-
return injectCleanupRuntimeImports(text)
|
|
17
|
-
}
|
|
18
|
-
return injectCleanupRuntimeImportsWithAliases(text, aliases)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func removeUnusedTypiaImports(text string) string {
|
|
22
|
-
prefix := cleanupImportScanPrefix(text)
|
|
23
|
-
if !strings.Contains(prefix, `require("typia")`) &&
|
|
24
|
-
!strings.Contains(prefix, `require('typia')`) &&
|
|
25
|
-
!strings.Contains(prefix, `from "typia"`) &&
|
|
26
|
-
!strings.Contains(prefix, `from 'typia'`) {
|
|
27
|
-
return text
|
|
28
|
-
}
|
|
29
|
-
type removal struct {
|
|
30
|
-
start int
|
|
31
|
-
end int
|
|
32
|
-
}
|
|
33
|
-
removals := []removal{}
|
|
34
|
-
offset := 0
|
|
35
|
-
for offset < len(prefix) {
|
|
36
|
-
next := strings.IndexByte(prefix[offset:], '\n')
|
|
37
|
-
lineEnd := len(prefix)
|
|
38
|
-
nextOffset := len(prefix)
|
|
39
|
-
if next >= 0 {
|
|
40
|
-
lineEnd = offset + next
|
|
41
|
-
nextOffset = lineEnd + 1
|
|
42
|
-
}
|
|
43
|
-
line := strings.TrimSuffix(prefix[offset:lineEnd], "\r")
|
|
44
|
-
if alias, ok := unusedTypiaImportAlias(line); ok &&
|
|
45
|
-
!cleanupIdentifierStillReferenced(text, alias, offset, nextOffset) {
|
|
46
|
-
removals = append(removals, removal{start: offset, end: nextOffset})
|
|
47
|
-
}
|
|
48
|
-
offset = nextOffset
|
|
49
|
-
}
|
|
50
|
-
for i := len(removals) - 1; i >= 0; i-- {
|
|
51
|
-
r := removals[i]
|
|
52
|
-
text = text[:r.start] + text[r.end:]
|
|
53
|
-
}
|
|
54
|
-
return text
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
func unusedTypiaImportAlias(line string) (string, bool) {
|
|
58
|
-
if strings.HasPrefix(line, "const ") {
|
|
59
|
-
index := strings.Index(line, " = ")
|
|
60
|
-
if index < 0 {
|
|
61
|
-
return "", false
|
|
62
|
-
}
|
|
63
|
-
alias := strings.TrimSpace(strings.TrimPrefix(line[:index], "const "))
|
|
64
|
-
if isTypiaImportAlias(alias) == false {
|
|
65
|
-
return "", false
|
|
66
|
-
}
|
|
67
|
-
value := strings.TrimSpace(line[index+3:])
|
|
68
|
-
return alias, value == `__importDefault(require("typia"));` ||
|
|
69
|
-
value == `__importDefault(require('typia'));` ||
|
|
70
|
-
value == `require("typia");` ||
|
|
71
|
-
value == `require('typia');`
|
|
72
|
-
}
|
|
73
|
-
if strings.HasPrefix(line, "import ") && strings.HasSuffix(line, ` from "typia";`) {
|
|
74
|
-
alias := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "import "), ` from "typia";`))
|
|
75
|
-
return alias, isTypiaImportAlias(alias)
|
|
76
|
-
}
|
|
77
|
-
if strings.HasPrefix(line, "import ") && strings.HasSuffix(line, ` from 'typia';`) {
|
|
78
|
-
alias := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "import "), ` from 'typia';`))
|
|
79
|
-
return alias, isTypiaImportAlias(alias)
|
|
80
|
-
}
|
|
81
|
-
return "", false
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
func isTypiaImportAlias(alias string) bool {
|
|
85
|
-
if alias == "typia" {
|
|
86
|
-
return true
|
|
87
|
-
}
|
|
88
|
-
if !strings.HasPrefix(alias, "typia_") {
|
|
89
|
-
return false
|
|
90
|
-
}
|
|
91
|
-
for _, ch := range alias[len("typia_"):] {
|
|
92
|
-
if ch < '0' || ch > '9' {
|
|
93
|
-
return false
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return len(alias) > len("typia_")
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
func cleanupIdentifierStillReferenced(text, alias string, lineStart, lineEnd int) bool {
|
|
100
|
-
return cleanupIdentifierAppears(text[:lineStart], alias) ||
|
|
101
|
-
cleanupIdentifierAppears(text[lineEnd:], alias)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
func cleanupIdentifierAppears(text string, alias string) bool {
|
|
105
|
-
offset := 0
|
|
106
|
-
for {
|
|
107
|
-
index := strings.Index(text[offset:], alias)
|
|
108
|
-
if index < 0 {
|
|
109
|
-
return false
|
|
110
|
-
}
|
|
111
|
-
start := offset + index
|
|
112
|
-
end := start + len(alias)
|
|
113
|
-
if (start == 0 || !isCleanupRuntimeAliasIdentifierByte(text[start-1])) &&
|
|
114
|
-
(end == len(text) || !isCleanupRuntimeAliasIdentifierByte(text[end])) {
|
|
115
|
-
return true
|
|
116
|
-
}
|
|
117
|
-
offset = end
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
func injectCleanupRuntimeImports(text string) string {
|
|
122
|
-
aliases := collectCleanupRuntimeAliases(text)
|
|
123
|
-
return injectCleanupRuntimeImportsWithAliases(text, aliases)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
func injectCleanupRuntimeImportsWithAliases(text string, aliases []string) string {
|
|
127
|
-
if len(aliases) == 0 {
|
|
128
|
-
return text
|
|
129
|
-
}
|
|
130
|
-
esModule := isCleanupESModuleOutput(text)
|
|
131
|
-
existing := collectExistingCleanupRuntimeImports(text)
|
|
132
|
-
imports := make([]string, 0, len(aliases))
|
|
133
|
-
for _, alias := range aliases {
|
|
134
|
-
module := cleanupRuntimeModuleOf(alias)
|
|
135
|
-
if existing[cleanupRuntimeImportKey(alias, module)] {
|
|
136
|
-
continue
|
|
137
|
-
}
|
|
138
|
-
name := cleanupRuntimeNameOf(alias)
|
|
139
|
-
if esModule {
|
|
140
|
-
imports = append(imports, fmt.Sprintf(`import { %s as %s } from %q;`, name, alias, module))
|
|
141
|
-
} else {
|
|
142
|
-
imports = append(imports, fmt.Sprintf(`const { %s: %s } = require(%q);`, name, alias, module))
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if len(imports) == 0 {
|
|
146
|
-
return text
|
|
147
|
-
}
|
|
148
|
-
index := cleanupRuntimeImportInsertionIndex(text, esModule)
|
|
149
|
-
return text[:index] + strings.Join(imports, "\n") + "\n" + text[index:]
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
func collectCleanupRuntimeAliases(text string) []string {
|
|
153
|
-
seen := map[string]bool{}
|
|
154
|
-
offset := 0
|
|
155
|
-
for {
|
|
156
|
-
index := strings.Index(text[offset:], cleanupRuntimeAliasPrefix)
|
|
157
|
-
if index < 0 {
|
|
158
|
-
break
|
|
159
|
-
}
|
|
160
|
-
start := offset + index
|
|
161
|
-
end := start + len(cleanupRuntimeAliasPrefix)
|
|
162
|
-
if start != 0 && isCleanupRuntimeAliasIdentifierByte(text[start-1]) {
|
|
163
|
-
offset = end
|
|
164
|
-
continue
|
|
165
|
-
}
|
|
166
|
-
for end < len(text) && isCleanupRuntimeAliasNameByte(text[end]) {
|
|
167
|
-
end++
|
|
168
|
-
}
|
|
169
|
-
if end != start+len(cleanupRuntimeAliasPrefix) &&
|
|
170
|
-
(end == len(text) || !isCleanupRuntimeAliasIdentifierByte(text[end])) {
|
|
171
|
-
seen[text[start:end]] = true
|
|
172
|
-
}
|
|
173
|
-
offset = end
|
|
174
|
-
}
|
|
175
|
-
return sortCleanupRuntimeAliases(seen)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
func sortCleanupRuntimeAliases(seen map[string]bool) []string {
|
|
179
|
-
aliases := make([]string, 0, len(seen))
|
|
180
|
-
for alias := range seen {
|
|
181
|
-
aliases = append(aliases, alias)
|
|
182
|
-
}
|
|
183
|
-
sort.SliceStable(aliases, func(i, j int) bool {
|
|
184
|
-
left, right := cleanupRuntimeAliasRank(aliases[i]), cleanupRuntimeAliasRank(aliases[j])
|
|
185
|
-
if left != right {
|
|
186
|
-
return left < right
|
|
187
|
-
}
|
|
188
|
-
return aliases[i] < aliases[j]
|
|
189
|
-
})
|
|
190
|
-
return aliases
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const cleanupRuntimeAliasPrefix = "__typia_transform_"
|
|
194
|
-
|
|
195
|
-
func isCleanupRuntimeAliasNameByte(value byte) bool {
|
|
196
|
-
return (value >= 'A' && value <= 'Z') ||
|
|
197
|
-
(value >= 'a' && value <= 'z') ||
|
|
198
|
-
(value >= '0' && value <= '9') ||
|
|
199
|
-
value == '_'
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
func isCleanupRuntimeAliasIdentifierByte(value byte) bool {
|
|
203
|
-
return isCleanupRuntimeAliasNameByte(value) || value == '$'
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
func cleanupRuntimeAliasRank(alias string) int {
|
|
207
|
-
name := strings.TrimPrefix(alias, cleanupRuntimeAliasPrefix)
|
|
208
|
-
switch {
|
|
209
|
-
case strings.HasPrefix(name, "_is"):
|
|
210
|
-
return 100
|
|
211
|
-
case strings.HasPrefix(name, "_assert"):
|
|
212
|
-
return 150
|
|
213
|
-
case strings.HasPrefix(name, "_randomFormat"):
|
|
214
|
-
return 200
|
|
215
|
-
case name == "_randomString":
|
|
216
|
-
return 210
|
|
217
|
-
case name == "_randomInteger":
|
|
218
|
-
return 220
|
|
219
|
-
case name == "_randomNumber":
|
|
220
|
-
return 221
|
|
221
|
-
case strings.HasPrefix(name, "_random"):
|
|
222
|
-
return 230
|
|
223
|
-
case name == "_validateReport":
|
|
224
|
-
return 800
|
|
225
|
-
case name == "_createStandardSchema":
|
|
226
|
-
return 900
|
|
227
|
-
}
|
|
228
|
-
return 500
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
func cleanupRuntimeModuleOf(alias string) string {
|
|
232
|
-
return "typia/lib/internal/" + cleanupRuntimeNameOf(alias)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
func cleanupRuntimeNameOf(alias string) string {
|
|
236
|
-
name := strings.TrimPrefix(alias, cleanupRuntimeAliasPrefix)
|
|
237
|
-
if !strings.HasPrefix(name, "_") {
|
|
238
|
-
name = "_" + name
|
|
239
|
-
}
|
|
240
|
-
return name
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
func collectExistingCleanupRuntimeImports(text string) map[string]bool {
|
|
244
|
-
output := map[string]bool{}
|
|
245
|
-
forEachCleanupImportLine(cleanupImportScanPrefix(text), func(line string) {
|
|
246
|
-
if !strings.Contains(line, cleanupRuntimeAliasPrefix) ||
|
|
247
|
-
!strings.Contains(line, "typia/lib/internal/") {
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
alias, ok := cleanupRuntimeImportAlias(line)
|
|
251
|
-
if !ok {
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
module, ok := cleanupRuntimeImportModule(line)
|
|
255
|
-
if !ok {
|
|
256
|
-
return
|
|
257
|
-
}
|
|
258
|
-
output[cleanupRuntimeImportKey(alias, module)] = true
|
|
259
|
-
})
|
|
260
|
-
return output
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
func cleanupImportScanPrefix(text string) string {
|
|
264
|
-
const limit = 1 << 20
|
|
265
|
-
if len(text) <= limit {
|
|
266
|
-
return text
|
|
267
|
-
}
|
|
268
|
-
return text[:limit]
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
func cleanupRuntimeImportKey(alias string, module string) string {
|
|
272
|
-
return alias + "\x00" + module
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
func forEachCleanupImportLine(text string, iterate func(string)) {
|
|
276
|
-
for len(text) > 0 {
|
|
277
|
-
next := strings.IndexByte(text, '\n')
|
|
278
|
-
line := text
|
|
279
|
-
if next >= 0 {
|
|
280
|
-
line = text[:next]
|
|
281
|
-
text = text[next+1:]
|
|
282
|
-
} else {
|
|
283
|
-
text = ""
|
|
284
|
-
}
|
|
285
|
-
iterate(strings.TrimSuffix(line, "\r"))
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
func cleanupRuntimeImportAlias(line string) (string, bool) {
|
|
290
|
-
switch {
|
|
291
|
-
case strings.HasPrefix(line, "const { "):
|
|
292
|
-
colon := strings.IndexByte(line, ':')
|
|
293
|
-
if colon < 0 {
|
|
294
|
-
return "", false
|
|
295
|
-
}
|
|
296
|
-
end := strings.IndexByte(line[colon+1:], '}')
|
|
297
|
-
if end < 0 {
|
|
298
|
-
return "", false
|
|
299
|
-
}
|
|
300
|
-
return cleanupFirstIdentifier(strings.TrimSpace(line[colon+1 : colon+1+end]))
|
|
301
|
-
case strings.HasPrefix(line, "import { "):
|
|
302
|
-
index := strings.LastIndex(line, " as ")
|
|
303
|
-
if index < 0 {
|
|
304
|
-
return "", false
|
|
305
|
-
}
|
|
306
|
-
end := strings.IndexByte(line[index+4:], '}')
|
|
307
|
-
if end < 0 {
|
|
308
|
-
return "", false
|
|
309
|
-
}
|
|
310
|
-
return cleanupFirstIdentifier(strings.TrimSpace(line[index+4 : index+4+end]))
|
|
311
|
-
case strings.HasPrefix(line, "const "):
|
|
312
|
-
rest := strings.TrimPrefix(line, "const ")
|
|
313
|
-
index := strings.Index(rest, " = require(")
|
|
314
|
-
if index < 0 {
|
|
315
|
-
return "", false
|
|
316
|
-
}
|
|
317
|
-
return cleanupFirstIdentifier(rest[:index])
|
|
318
|
-
case strings.HasPrefix(line, "import * as "):
|
|
319
|
-
rest := strings.TrimPrefix(line, "import * as ")
|
|
320
|
-
index := strings.Index(rest, " from ")
|
|
321
|
-
if index < 0 {
|
|
322
|
-
return "", false
|
|
323
|
-
}
|
|
324
|
-
return cleanupFirstIdentifier(rest[:index])
|
|
325
|
-
default:
|
|
326
|
-
return "", false
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
func cleanupFirstIdentifier(text string) (string, bool) {
|
|
331
|
-
fields := strings.Fields(text)
|
|
332
|
-
if len(fields) == 0 {
|
|
333
|
-
return "", false
|
|
334
|
-
}
|
|
335
|
-
name := strings.Trim(fields[0], " \t{}")
|
|
336
|
-
if !strings.HasPrefix(name, cleanupRuntimeAliasPrefix) {
|
|
337
|
-
return "", false
|
|
338
|
-
}
|
|
339
|
-
return name, true
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
func cleanupRuntimeImportModule(line string) (string, bool) {
|
|
343
|
-
for _, marker := range []string{`require("`, `require('`, ` from "`, ` from '`} {
|
|
344
|
-
index := strings.LastIndex(line, marker)
|
|
345
|
-
if index < 0 {
|
|
346
|
-
continue
|
|
347
|
-
}
|
|
348
|
-
start := index + len(marker)
|
|
349
|
-
quote := marker[len(marker)-1]
|
|
350
|
-
end := strings.IndexByte(line[start:], quote)
|
|
351
|
-
if end < 0 {
|
|
352
|
-
return "", false
|
|
353
|
-
}
|
|
354
|
-
module := line[start : start+end]
|
|
355
|
-
if !strings.HasPrefix(module, "typia/lib/internal/") {
|
|
356
|
-
return "", false
|
|
357
|
-
}
|
|
358
|
-
return module, true
|
|
359
|
-
}
|
|
360
|
-
return "", false
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
func isCleanupESModuleOutput(text string) bool {
|
|
364
|
-
output := false
|
|
365
|
-
forEachCleanupImportLine(cleanupImportScanPrefix(text), func(line string) {
|
|
366
|
-
if strings.HasPrefix(line, "import ") ||
|
|
367
|
-
strings.HasPrefix(line, "import{") ||
|
|
368
|
-
strings.HasPrefix(line, "import*") ||
|
|
369
|
-
strings.HasPrefix(line, "export ") {
|
|
370
|
-
output = true
|
|
371
|
-
}
|
|
372
|
-
})
|
|
373
|
-
return output
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
func cleanupRuntimeImportInsertionIndex(text string, esModule bool) int {
|
|
377
|
-
index := 0
|
|
378
|
-
if strings.HasPrefix(text, "#!") {
|
|
379
|
-
if next := strings.IndexByte(text, '\n'); next >= 0 {
|
|
380
|
-
index = next + 1
|
|
381
|
-
} else {
|
|
382
|
-
return len(text)
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if esModule {
|
|
386
|
-
return index
|
|
387
|
-
}
|
|
388
|
-
for {
|
|
389
|
-
next := consumeCleanupRuntimeImportPrefix(text[index:])
|
|
390
|
-
if next == 0 {
|
|
391
|
-
return index
|
|
392
|
-
}
|
|
393
|
-
index += next
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
func consumeCleanupRuntimeImportPrefix(text string) int {
|
|
398
|
-
for _, prefix := range []string{
|
|
399
|
-
"\"use strict\";\n",
|
|
400
|
-
"'use strict';\n",
|
|
401
|
-
"/* @ttsc-rewritten */\n",
|
|
402
|
-
} {
|
|
403
|
-
if strings.HasPrefix(text, prefix) {
|
|
404
|
-
return len(prefix)
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
return 0
|
|
408
|
-
}
|
|
1
|
+
package transform
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"sort"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func cleanupTransformedText(text string) string {
|
|
10
|
+
return cleanupTransformedTextWithRuntimeAliases(text, nil)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func cleanupTransformedTextWithRuntimeAliases(text string, aliases []string) string {
|
|
14
|
+
text = removeUnusedTypiaImports(text)
|
|
15
|
+
if aliases == nil {
|
|
16
|
+
return injectCleanupRuntimeImports(text)
|
|
17
|
+
}
|
|
18
|
+
return injectCleanupRuntimeImportsWithAliases(text, aliases)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func removeUnusedTypiaImports(text string) string {
|
|
22
|
+
prefix := cleanupImportScanPrefix(text)
|
|
23
|
+
if !strings.Contains(prefix, `require("typia")`) &&
|
|
24
|
+
!strings.Contains(prefix, `require('typia')`) &&
|
|
25
|
+
!strings.Contains(prefix, `from "typia"`) &&
|
|
26
|
+
!strings.Contains(prefix, `from 'typia'`) {
|
|
27
|
+
return text
|
|
28
|
+
}
|
|
29
|
+
type removal struct {
|
|
30
|
+
start int
|
|
31
|
+
end int
|
|
32
|
+
}
|
|
33
|
+
removals := []removal{}
|
|
34
|
+
offset := 0
|
|
35
|
+
for offset < len(prefix) {
|
|
36
|
+
next := strings.IndexByte(prefix[offset:], '\n')
|
|
37
|
+
lineEnd := len(prefix)
|
|
38
|
+
nextOffset := len(prefix)
|
|
39
|
+
if next >= 0 {
|
|
40
|
+
lineEnd = offset + next
|
|
41
|
+
nextOffset = lineEnd + 1
|
|
42
|
+
}
|
|
43
|
+
line := strings.TrimSuffix(prefix[offset:lineEnd], "\r")
|
|
44
|
+
if alias, ok := unusedTypiaImportAlias(line); ok &&
|
|
45
|
+
!cleanupIdentifierStillReferenced(text, alias, offset, nextOffset) {
|
|
46
|
+
removals = append(removals, removal{start: offset, end: nextOffset})
|
|
47
|
+
}
|
|
48
|
+
offset = nextOffset
|
|
49
|
+
}
|
|
50
|
+
for i := len(removals) - 1; i >= 0; i-- {
|
|
51
|
+
r := removals[i]
|
|
52
|
+
text = text[:r.start] + text[r.end:]
|
|
53
|
+
}
|
|
54
|
+
return text
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func unusedTypiaImportAlias(line string) (string, bool) {
|
|
58
|
+
if strings.HasPrefix(line, "const ") {
|
|
59
|
+
index := strings.Index(line, " = ")
|
|
60
|
+
if index < 0 {
|
|
61
|
+
return "", false
|
|
62
|
+
}
|
|
63
|
+
alias := strings.TrimSpace(strings.TrimPrefix(line[:index], "const "))
|
|
64
|
+
if isTypiaImportAlias(alias) == false {
|
|
65
|
+
return "", false
|
|
66
|
+
}
|
|
67
|
+
value := strings.TrimSpace(line[index+3:])
|
|
68
|
+
return alias, value == `__importDefault(require("typia"));` ||
|
|
69
|
+
value == `__importDefault(require('typia'));` ||
|
|
70
|
+
value == `require("typia");` ||
|
|
71
|
+
value == `require('typia');`
|
|
72
|
+
}
|
|
73
|
+
if strings.HasPrefix(line, "import ") && strings.HasSuffix(line, ` from "typia";`) {
|
|
74
|
+
alias := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "import "), ` from "typia";`))
|
|
75
|
+
return alias, isTypiaImportAlias(alias)
|
|
76
|
+
}
|
|
77
|
+
if strings.HasPrefix(line, "import ") && strings.HasSuffix(line, ` from 'typia';`) {
|
|
78
|
+
alias := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "import "), ` from 'typia';`))
|
|
79
|
+
return alias, isTypiaImportAlias(alias)
|
|
80
|
+
}
|
|
81
|
+
return "", false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func isTypiaImportAlias(alias string) bool {
|
|
85
|
+
if alias == "typia" {
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
if !strings.HasPrefix(alias, "typia_") {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
for _, ch := range alias[len("typia_"):] {
|
|
92
|
+
if ch < '0' || ch > '9' {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return len(alias) > len("typia_")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func cleanupIdentifierStillReferenced(text, alias string, lineStart, lineEnd int) bool {
|
|
100
|
+
return cleanupIdentifierAppears(text[:lineStart], alias) ||
|
|
101
|
+
cleanupIdentifierAppears(text[lineEnd:], alias)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func cleanupIdentifierAppears(text string, alias string) bool {
|
|
105
|
+
offset := 0
|
|
106
|
+
for {
|
|
107
|
+
index := strings.Index(text[offset:], alias)
|
|
108
|
+
if index < 0 {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
start := offset + index
|
|
112
|
+
end := start + len(alias)
|
|
113
|
+
if (start == 0 || !isCleanupRuntimeAliasIdentifierByte(text[start-1])) &&
|
|
114
|
+
(end == len(text) || !isCleanupRuntimeAliasIdentifierByte(text[end])) {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
offset = end
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func injectCleanupRuntimeImports(text string) string {
|
|
122
|
+
aliases := collectCleanupRuntimeAliases(text)
|
|
123
|
+
return injectCleanupRuntimeImportsWithAliases(text, aliases)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func injectCleanupRuntimeImportsWithAliases(text string, aliases []string) string {
|
|
127
|
+
if len(aliases) == 0 {
|
|
128
|
+
return text
|
|
129
|
+
}
|
|
130
|
+
esModule := isCleanupESModuleOutput(text)
|
|
131
|
+
existing := collectExistingCleanupRuntimeImports(text)
|
|
132
|
+
imports := make([]string, 0, len(aliases))
|
|
133
|
+
for _, alias := range aliases {
|
|
134
|
+
module := cleanupRuntimeModuleOf(alias)
|
|
135
|
+
if existing[cleanupRuntimeImportKey(alias, module)] {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
name := cleanupRuntimeNameOf(alias)
|
|
139
|
+
if esModule {
|
|
140
|
+
imports = append(imports, fmt.Sprintf(`import { %s as %s } from %q;`, name, alias, module))
|
|
141
|
+
} else {
|
|
142
|
+
imports = append(imports, fmt.Sprintf(`const { %s: %s } = require(%q);`, name, alias, module))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if len(imports) == 0 {
|
|
146
|
+
return text
|
|
147
|
+
}
|
|
148
|
+
index := cleanupRuntimeImportInsertionIndex(text, esModule)
|
|
149
|
+
return text[:index] + strings.Join(imports, "\n") + "\n" + text[index:]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func collectCleanupRuntimeAliases(text string) []string {
|
|
153
|
+
seen := map[string]bool{}
|
|
154
|
+
offset := 0
|
|
155
|
+
for {
|
|
156
|
+
index := strings.Index(text[offset:], cleanupRuntimeAliasPrefix)
|
|
157
|
+
if index < 0 {
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
start := offset + index
|
|
161
|
+
end := start + len(cleanupRuntimeAliasPrefix)
|
|
162
|
+
if start != 0 && isCleanupRuntimeAliasIdentifierByte(text[start-1]) {
|
|
163
|
+
offset = end
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
for end < len(text) && isCleanupRuntimeAliasNameByte(text[end]) {
|
|
167
|
+
end++
|
|
168
|
+
}
|
|
169
|
+
if end != start+len(cleanupRuntimeAliasPrefix) &&
|
|
170
|
+
(end == len(text) || !isCleanupRuntimeAliasIdentifierByte(text[end])) {
|
|
171
|
+
seen[text[start:end]] = true
|
|
172
|
+
}
|
|
173
|
+
offset = end
|
|
174
|
+
}
|
|
175
|
+
return sortCleanupRuntimeAliases(seen)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func sortCleanupRuntimeAliases(seen map[string]bool) []string {
|
|
179
|
+
aliases := make([]string, 0, len(seen))
|
|
180
|
+
for alias := range seen {
|
|
181
|
+
aliases = append(aliases, alias)
|
|
182
|
+
}
|
|
183
|
+
sort.SliceStable(aliases, func(i, j int) bool {
|
|
184
|
+
left, right := cleanupRuntimeAliasRank(aliases[i]), cleanupRuntimeAliasRank(aliases[j])
|
|
185
|
+
if left != right {
|
|
186
|
+
return left < right
|
|
187
|
+
}
|
|
188
|
+
return aliases[i] < aliases[j]
|
|
189
|
+
})
|
|
190
|
+
return aliases
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const cleanupRuntimeAliasPrefix = "__typia_transform_"
|
|
194
|
+
|
|
195
|
+
func isCleanupRuntimeAliasNameByte(value byte) bool {
|
|
196
|
+
return (value >= 'A' && value <= 'Z') ||
|
|
197
|
+
(value >= 'a' && value <= 'z') ||
|
|
198
|
+
(value >= '0' && value <= '9') ||
|
|
199
|
+
value == '_'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func isCleanupRuntimeAliasIdentifierByte(value byte) bool {
|
|
203
|
+
return isCleanupRuntimeAliasNameByte(value) || value == '$'
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
func cleanupRuntimeAliasRank(alias string) int {
|
|
207
|
+
name := strings.TrimPrefix(alias, cleanupRuntimeAliasPrefix)
|
|
208
|
+
switch {
|
|
209
|
+
case strings.HasPrefix(name, "_is"):
|
|
210
|
+
return 100
|
|
211
|
+
case strings.HasPrefix(name, "_assert"):
|
|
212
|
+
return 150
|
|
213
|
+
case strings.HasPrefix(name, "_randomFormat"):
|
|
214
|
+
return 200
|
|
215
|
+
case name == "_randomString":
|
|
216
|
+
return 210
|
|
217
|
+
case name == "_randomInteger":
|
|
218
|
+
return 220
|
|
219
|
+
case name == "_randomNumber":
|
|
220
|
+
return 221
|
|
221
|
+
case strings.HasPrefix(name, "_random"):
|
|
222
|
+
return 230
|
|
223
|
+
case name == "_validateReport":
|
|
224
|
+
return 800
|
|
225
|
+
case name == "_createStandardSchema":
|
|
226
|
+
return 900
|
|
227
|
+
}
|
|
228
|
+
return 500
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func cleanupRuntimeModuleOf(alias string) string {
|
|
232
|
+
return "typia/lib/internal/" + cleanupRuntimeNameOf(alias)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func cleanupRuntimeNameOf(alias string) string {
|
|
236
|
+
name := strings.TrimPrefix(alias, cleanupRuntimeAliasPrefix)
|
|
237
|
+
if !strings.HasPrefix(name, "_") {
|
|
238
|
+
name = "_" + name
|
|
239
|
+
}
|
|
240
|
+
return name
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func collectExistingCleanupRuntimeImports(text string) map[string]bool {
|
|
244
|
+
output := map[string]bool{}
|
|
245
|
+
forEachCleanupImportLine(cleanupImportScanPrefix(text), func(line string) {
|
|
246
|
+
if !strings.Contains(line, cleanupRuntimeAliasPrefix) ||
|
|
247
|
+
!strings.Contains(line, "typia/lib/internal/") {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
alias, ok := cleanupRuntimeImportAlias(line)
|
|
251
|
+
if !ok {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
module, ok := cleanupRuntimeImportModule(line)
|
|
255
|
+
if !ok {
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
output[cleanupRuntimeImportKey(alias, module)] = true
|
|
259
|
+
})
|
|
260
|
+
return output
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func cleanupImportScanPrefix(text string) string {
|
|
264
|
+
const limit = 1 << 20
|
|
265
|
+
if len(text) <= limit {
|
|
266
|
+
return text
|
|
267
|
+
}
|
|
268
|
+
return text[:limit]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func cleanupRuntimeImportKey(alias string, module string) string {
|
|
272
|
+
return alias + "\x00" + module
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
func forEachCleanupImportLine(text string, iterate func(string)) {
|
|
276
|
+
for len(text) > 0 {
|
|
277
|
+
next := strings.IndexByte(text, '\n')
|
|
278
|
+
line := text
|
|
279
|
+
if next >= 0 {
|
|
280
|
+
line = text[:next]
|
|
281
|
+
text = text[next+1:]
|
|
282
|
+
} else {
|
|
283
|
+
text = ""
|
|
284
|
+
}
|
|
285
|
+
iterate(strings.TrimSuffix(line, "\r"))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func cleanupRuntimeImportAlias(line string) (string, bool) {
|
|
290
|
+
switch {
|
|
291
|
+
case strings.HasPrefix(line, "const { "):
|
|
292
|
+
colon := strings.IndexByte(line, ':')
|
|
293
|
+
if colon < 0 {
|
|
294
|
+
return "", false
|
|
295
|
+
}
|
|
296
|
+
end := strings.IndexByte(line[colon+1:], '}')
|
|
297
|
+
if end < 0 {
|
|
298
|
+
return "", false
|
|
299
|
+
}
|
|
300
|
+
return cleanupFirstIdentifier(strings.TrimSpace(line[colon+1 : colon+1+end]))
|
|
301
|
+
case strings.HasPrefix(line, "import { "):
|
|
302
|
+
index := strings.LastIndex(line, " as ")
|
|
303
|
+
if index < 0 {
|
|
304
|
+
return "", false
|
|
305
|
+
}
|
|
306
|
+
end := strings.IndexByte(line[index+4:], '}')
|
|
307
|
+
if end < 0 {
|
|
308
|
+
return "", false
|
|
309
|
+
}
|
|
310
|
+
return cleanupFirstIdentifier(strings.TrimSpace(line[index+4 : index+4+end]))
|
|
311
|
+
case strings.HasPrefix(line, "const "):
|
|
312
|
+
rest := strings.TrimPrefix(line, "const ")
|
|
313
|
+
index := strings.Index(rest, " = require(")
|
|
314
|
+
if index < 0 {
|
|
315
|
+
return "", false
|
|
316
|
+
}
|
|
317
|
+
return cleanupFirstIdentifier(rest[:index])
|
|
318
|
+
case strings.HasPrefix(line, "import * as "):
|
|
319
|
+
rest := strings.TrimPrefix(line, "import * as ")
|
|
320
|
+
index := strings.Index(rest, " from ")
|
|
321
|
+
if index < 0 {
|
|
322
|
+
return "", false
|
|
323
|
+
}
|
|
324
|
+
return cleanupFirstIdentifier(rest[:index])
|
|
325
|
+
default:
|
|
326
|
+
return "", false
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func cleanupFirstIdentifier(text string) (string, bool) {
|
|
331
|
+
fields := strings.Fields(text)
|
|
332
|
+
if len(fields) == 0 {
|
|
333
|
+
return "", false
|
|
334
|
+
}
|
|
335
|
+
name := strings.Trim(fields[0], " \t{}")
|
|
336
|
+
if !strings.HasPrefix(name, cleanupRuntimeAliasPrefix) {
|
|
337
|
+
return "", false
|
|
338
|
+
}
|
|
339
|
+
return name, true
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
func cleanupRuntimeImportModule(line string) (string, bool) {
|
|
343
|
+
for _, marker := range []string{`require("`, `require('`, ` from "`, ` from '`} {
|
|
344
|
+
index := strings.LastIndex(line, marker)
|
|
345
|
+
if index < 0 {
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
start := index + len(marker)
|
|
349
|
+
quote := marker[len(marker)-1]
|
|
350
|
+
end := strings.IndexByte(line[start:], quote)
|
|
351
|
+
if end < 0 {
|
|
352
|
+
return "", false
|
|
353
|
+
}
|
|
354
|
+
module := line[start : start+end]
|
|
355
|
+
if !strings.HasPrefix(module, "typia/lib/internal/") {
|
|
356
|
+
return "", false
|
|
357
|
+
}
|
|
358
|
+
return module, true
|
|
359
|
+
}
|
|
360
|
+
return "", false
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
func isCleanupESModuleOutput(text string) bool {
|
|
364
|
+
output := false
|
|
365
|
+
forEachCleanupImportLine(cleanupImportScanPrefix(text), func(line string) {
|
|
366
|
+
if strings.HasPrefix(line, "import ") ||
|
|
367
|
+
strings.HasPrefix(line, "import{") ||
|
|
368
|
+
strings.HasPrefix(line, "import*") ||
|
|
369
|
+
strings.HasPrefix(line, "export ") {
|
|
370
|
+
output = true
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
return output
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func cleanupRuntimeImportInsertionIndex(text string, esModule bool) int {
|
|
377
|
+
index := 0
|
|
378
|
+
if strings.HasPrefix(text, "#!") {
|
|
379
|
+
if next := strings.IndexByte(text, '\n'); next >= 0 {
|
|
380
|
+
index = next + 1
|
|
381
|
+
} else {
|
|
382
|
+
return len(text)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if esModule {
|
|
386
|
+
return index
|
|
387
|
+
}
|
|
388
|
+
for {
|
|
389
|
+
next := consumeCleanupRuntimeImportPrefix(text[index:])
|
|
390
|
+
if next == 0 {
|
|
391
|
+
return index
|
|
392
|
+
}
|
|
393
|
+
index += next
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
func consumeCleanupRuntimeImportPrefix(text string) int {
|
|
398
|
+
for _, prefix := range []string{
|
|
399
|
+
"\"use strict\";\n",
|
|
400
|
+
"'use strict';\n",
|
|
401
|
+
"/* @ttsc-rewritten */\n",
|
|
402
|
+
} {
|
|
403
|
+
if strings.HasPrefix(text, prefix) {
|
|
404
|
+
return len(prefix)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return 0
|
|
408
|
+
}
|