@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.
Files changed (116) hide show
  1. package/LICENSE +21 -21
  2. package/MIGRATION.md +169 -169
  3. package/README.md +93 -93
  4. package/lib/adaptors/McpAdaptor.d.ts +75 -0
  5. package/lib/adaptors/McpAdaptor.js +257 -0
  6. package/lib/adaptors/McpAdaptor.js.map +1 -0
  7. package/lib/adaptors/WebSocketAdaptor.js +4 -4
  8. package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
  9. package/lib/decorators/McpRoute.d.ts +69 -0
  10. package/lib/decorators/McpRoute.js +58 -0
  11. package/lib/decorators/McpRoute.js.map +1 -0
  12. package/lib/decorators/TypedParam.js +4 -4
  13. package/lib/decorators/TypedParam.js.map +1 -1
  14. package/lib/decorators/TypedRoute.js +1 -1
  15. package/lib/decorators/TypedRoute.js.map +1 -1
  16. package/lib/decorators/internal/IMcpRouteReflect.d.ts +2 -0
  17. package/lib/decorators/internal/IMcpRouteReflect.js +3 -0
  18. package/lib/decorators/internal/IMcpRouteReflect.js.map +1 -0
  19. package/lib/decorators/internal/get_path_and_querify.js +4 -4
  20. package/lib/decorators/internal/get_path_and_querify.js.map +1 -1
  21. package/lib/decorators/internal/get_path_and_stringify.js +4 -4
  22. package/lib/decorators/internal/get_path_and_stringify.js.map +1 -1
  23. package/lib/decorators/internal/load_controller.js +34 -65
  24. package/lib/decorators/internal/load_controller.js.map +1 -1
  25. package/lib/decorators/internal/validate_request_body.js +4 -4
  26. package/lib/decorators/internal/validate_request_body.js.map +1 -1
  27. package/lib/decorators/internal/validate_request_form_data.js +4 -4
  28. package/lib/decorators/internal/validate_request_form_data.js.map +1 -1
  29. package/lib/decorators/internal/validate_request_headers.js +4 -4
  30. package/lib/decorators/internal/validate_request_headers.js.map +1 -1
  31. package/lib/decorators/internal/validate_request_query.js +4 -4
  32. package/lib/decorators/internal/validate_request_query.js.map +1 -1
  33. package/lib/module.d.ts +2 -0
  34. package/lib/module.js +2 -0
  35. package/lib/module.js.map +1 -1
  36. package/native/cmd/ttsc-nestia/main.go +11 -11
  37. package/native/go.mod +32 -32
  38. package/native/go.sum +54 -54
  39. package/native/plugin/plan.go +102 -102
  40. package/native/transform/ast.go +32 -32
  41. package/native/transform/build.go +380 -444
  42. package/native/transform/cleanup.go +408 -408
  43. package/native/transform/contributor.go +97 -68
  44. package/native/transform/core_querify.go +231 -227
  45. package/native/transform/core_transform.go +1996 -1713
  46. package/native/transform/core_websocket.go +115 -115
  47. package/native/transform/exports.go +13 -13
  48. package/native/transform/mcp_transform.go +414 -0
  49. package/native/transform/node_transform.go +357 -0
  50. package/native/transform/path_rewrite.go +285 -285
  51. package/native/transform/printer.go +244 -244
  52. package/native/transform/rewrite.go +668 -662
  53. package/native/transform/run.go +73 -73
  54. package/native/transform/transform.go +336 -403
  55. package/native/transform/typia_fast.go +352 -326
  56. package/native/transform/typia_replacement.go +24 -24
  57. package/native/transform.cjs +43 -43
  58. package/package.json +15 -8
  59. package/src/adaptors/McpAdaptor.ts +276 -0
  60. package/src/adaptors/WebSocketAdaptor.ts +429 -429
  61. package/src/decorators/DynamicModule.ts +44 -44
  62. package/src/decorators/EncryptedBody.ts +97 -97
  63. package/src/decorators/EncryptedController.ts +40 -40
  64. package/src/decorators/EncryptedModule.ts +98 -98
  65. package/src/decorators/EncryptedRoute.ts +213 -213
  66. package/src/decorators/HumanRoute.ts +21 -21
  67. package/src/decorators/McpRoute.ts +154 -0
  68. package/src/decorators/NoTransformConfigurationError.ts +40 -40
  69. package/src/decorators/PlainBody.ts +76 -76
  70. package/src/decorators/SwaggerCustomizer.ts +97 -97
  71. package/src/decorators/SwaggerExample.ts +180 -180
  72. package/src/decorators/TypedBody.ts +57 -57
  73. package/src/decorators/TypedException.ts +147 -147
  74. package/src/decorators/TypedFormData.ts +187 -187
  75. package/src/decorators/TypedHeaders.ts +66 -66
  76. package/src/decorators/TypedParam.ts +77 -77
  77. package/src/decorators/TypedQuery.ts +234 -234
  78. package/src/decorators/TypedRoute.ts +198 -196
  79. package/src/decorators/WebSocketRoute.ts +242 -242
  80. package/src/decorators/doNotThrowTransformError.ts +5 -5
  81. package/src/decorators/internal/EncryptedConstant.ts +2 -2
  82. package/src/decorators/internal/IMcpRouteReflect.ts +40 -0
  83. package/src/decorators/internal/IWebSocketRouteReflect.ts +23 -23
  84. package/src/decorators/internal/get_path_and_querify.ts +94 -94
  85. package/src/decorators/internal/get_path_and_stringify.ts +110 -110
  86. package/src/decorators/internal/get_text_body.ts +16 -16
  87. package/src/decorators/internal/headers_to_object.ts +11 -11
  88. package/src/decorators/internal/is_request_body_undefined.ts +12 -12
  89. package/src/decorators/internal/load_controller.ts +91 -76
  90. package/src/decorators/internal/route_error.ts +43 -43
  91. package/src/decorators/internal/validate_request_body.ts +64 -64
  92. package/src/decorators/internal/validate_request_form_data.ts +67 -67
  93. package/src/decorators/internal/validate_request_headers.ts +76 -76
  94. package/src/decorators/internal/validate_request_query.ts +83 -83
  95. package/src/index.ts +5 -5
  96. package/src/module.ts +25 -23
  97. package/src/options/IRequestBodyValidator.ts +20 -20
  98. package/src/options/IRequestFormDataProps.ts +27 -27
  99. package/src/options/IRequestHeadersValidator.ts +22 -22
  100. package/src/options/IRequestQueryValidator.ts +20 -20
  101. package/src/options/IResponseBodyQuerifier.ts +25 -25
  102. package/src/options/IResponseBodyStringifier.ts +30 -30
  103. package/src/transform.ts +101 -101
  104. package/src/typings/Creator.ts +3 -3
  105. package/src/typings/get-function-location.d.ts +7 -7
  106. package/src/utils/ArrayUtil.ts +7 -7
  107. package/src/utils/ExceptionManager.ts +115 -115
  108. package/src/utils/Singleton.ts +16 -16
  109. package/src/utils/SourceFinder.ts +54 -54
  110. package/src/utils/VersioningStrategy.ts +27 -27
  111. package/native/transform/cleanup_test.go +0 -76
  112. package/native/transform/commonjs_import_alias_test.go +0 -49
  113. package/native/transform/core_dispatch_test.go +0 -127
  114. package/native/transform/path_rewrite_test.go +0 -243
  115. package/native/transform/rewrite_test.go +0 -118
  116. package/native/transform/rewrite_unique_base_test.go +0 -48
@@ -1,403 +1,336 @@
1
- package transform
2
-
3
- import (
4
- "bytes"
5
- "encoding/json"
6
- "flag"
7
- "fmt"
8
- "os"
9
- "path/filepath"
10
- "regexp"
11
- "sort"
12
- "strings"
13
-
14
- "github.com/samchon/nestia/packages/core/native/plugin"
15
- "github.com/samchon/ttsc/packages/ttsc/driver"
16
- typiaadapter "github.com/samchon/typia/packages/typia/native/adapter"
17
- )
18
-
19
- type transformProjectOutput struct {
20
- Diagnostics []transformCompilerDiagnostic `json:"diagnostics,omitempty"`
21
- TypeScript map[string]string `json:"typescript"`
22
- }
23
-
24
- type transformCompilerDiagnostic struct {
25
- File *string `json:"file"`
26
- Category string `json:"category"`
27
- Code string `json:"code"`
28
- Line int `json:"line,omitempty"`
29
- Character int `json:"character,omitempty"`
30
- MessageText string `json:"messageText"`
31
- }
32
-
33
- func runTransform(args []string) int {
34
- fs := flag.NewFlagSet("transform", flag.ContinueOnError)
35
- fs.SetOutput(stderr)
36
- file := fs.String("file", "", "absolute or cwd-relative path of the .ts file to transform")
37
- tsconfigPath := fs.String("tsconfig", "tsconfig.json", "tsconfig.json owning --file")
38
- cwdOverride := fs.String("cwd", "", "override the working directory")
39
- out := fs.String("out", "", "write output to PATH")
40
- output := fs.String("output", "ts", "transform output kind: ts")
41
- pluginsJSON := fs.String("plugins-json", "", "ordered ttsc plugin payload")
42
- if err := fs.Parse(args); err != nil {
43
- return 2
44
- }
45
- if *output != "ts" {
46
- fmt.Fprintf(stderr, "ttsc-nestia transform: unknown --output value %q\n", *output)
47
- return 2
48
- }
49
- plan, err := plugin.ParsePlan(*pluginsJSON)
50
- if err != nil {
51
- fmt.Fprintf(stderr, "ttsc-nestia transform: %v\n", err)
52
- return 2
53
- }
54
-
55
- cwd, ok := resolveCWD("ttsc-nestia transform", *cwdOverride)
56
- if !ok {
57
- return 2
58
- }
59
- prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
60
- ForceNoEmit: true,
61
- })
62
- if err != nil {
63
- fmt.Fprintf(stderr, "ttsc-nestia transform: %v\n", err)
64
- return 2
65
- }
66
- if len(diags) > 0 {
67
- driver.WritePrettyDiagnostics(stderr, diags, cwd)
68
- return 2
69
- }
70
- defer prog.Close()
71
-
72
- if *file == "" {
73
- if *out != "" {
74
- fmt.Fprintln(stderr, "ttsc-nestia transform: --out requires --file")
75
- return 2
76
- }
77
- return runTransformProject(prog, cwd, *tsconfigPath, plan)
78
- }
79
-
80
- absFile := *file
81
- if !filepath.IsAbs(absFile) {
82
- absFile = filepath.Join(cwd, absFile)
83
- }
84
- absFile = filepath.ToSlash(absFile)
85
- target := prog.SourceFile(absFile)
86
- if target == nil {
87
- fmt.Fprintf(stderr, "ttsc-nestia transform: source file is not in program: %s\n", absFile)
88
- return 2
89
- }
90
- rewrites, diagnostics := collectTypiaSourceRewrites(
91
- prog,
92
- cwd,
93
- absFile,
94
- readTypiaPluginOptions(cwd, *tsconfigPath),
95
- )
96
- if len(diagnostics) > 0 {
97
- WriteTypiaTransformDiagnostics(stderr, diagnostics, cwd)
98
- return 3
99
- }
100
- coreRewriteMap, coreDiagnostics := collectNestiaCoreSourceRewriteMap(prog, plan, absFile)
101
- if len(coreDiagnostics) > 0 {
102
- WriteTypiaTransformDiagnostics(stderr, coreDiagnostics, cwd)
103
- return 3
104
- }
105
- rewrites = append(rewrites, coreRewriteMap[filepath.ToSlash(absFile)]...)
106
- contributorRewriteMap, contributorDiagnostics := collectContributorSourceRewriteMap(prog, plan, absFile)
107
- if len(contributorDiagnostics) > 0 {
108
- WriteTypiaTransformDiagnostics(stderr, contributorDiagnostics, cwd)
109
- return 3
110
- }
111
- rewrites = append(rewrites, contributorRewriteMap[filepath.ToSlash(absFile)]...)
112
- source, ok := SourceFileText(target)
113
- if !ok {
114
- fmt.Fprintf(stderr, "ttsc-nestia transform: source text is unavailable for %s\n", absFile)
115
- return 3
116
- }
117
- source, err = ApplySourceRewrites(source, rewrites)
118
- if err != nil {
119
- fmt.Fprintf(stderr, "ttsc-nestia transform: source rewrite: %v\n", err)
120
- return 3
121
- }
122
- source = cleanupTypeScriptTransformText(source)
123
- if *out == "" {
124
- if _, err := bytes.NewBufferString(source).WriteTo(stdout); err != nil {
125
- fmt.Fprintf(stderr, "ttsc-nestia transform: write stdout: %v\n", err)
126
- return 3
127
- }
128
- return 0
129
- }
130
- if dir := filepath.Dir(*out); dir != "" {
131
- if err := os.MkdirAll(dir, 0o755); err != nil {
132
- fmt.Fprintf(stderr, "ttsc-nestia transform: mkdir: %v\n", err)
133
- return 3
134
- }
135
- }
136
- if err := os.WriteFile(*out, []byte(source), 0o644); err != nil {
137
- fmt.Fprintf(stderr, "ttsc-nestia transform: write %s: %v\n", *out, err)
138
- return 3
139
- }
140
- return 0
141
- }
142
-
143
- func runTransformProject(prog *driver.Program, cwd string, tsconfigPath string, plan plugin.Plan) int {
144
- rewrites, diags := collectTypiaSourceRewriteMap(
145
- prog,
146
- readTypiaPluginOptions(cwd, tsconfigPath),
147
- )
148
- coreRewriteMap, coreDiags := collectNestiaCoreSourceRewriteMap(prog, plan, "")
149
- for file, entries := range coreRewriteMap {
150
- rewrites[file] = append(rewrites[file], entries...)
151
- }
152
- contributorRewriteMap, contributorDiags := collectContributorSourceRewriteMap(prog, plan, "")
153
- for file, entries := range contributorRewriteMap {
154
- rewrites[file] = append(rewrites[file], entries...)
155
- }
156
- diags = append(diags, coreDiags...)
157
- diags = append(diags, contributorDiags...)
158
- output := transformProjectOutput{
159
- Diagnostics: make([]transformCompilerDiagnostic, 0, len(diags)),
160
- TypeScript: map[string]string{},
161
- }
162
- for _, diag := range diags {
163
- output.Diagnostics = append(output.Diagnostics, transformDiagnosticToCompilerDiagnostic(diag))
164
- }
165
- for _, file := range prog.SourceFiles() {
166
- filename := filepath.ToSlash(file.FileName())
167
- source, ok := SourceFileText(file)
168
- if !ok {
169
- output.Diagnostics = append(
170
- output.Diagnostics,
171
- newTransformCompilerDiagnostic(filename, "nestia.transform", "source text is unavailable"),
172
- )
173
- continue
174
- }
175
- transformed, err := ApplySourceRewrites(source, rewrites[filename])
176
- if err != nil {
177
- output.Diagnostics = append(
178
- output.Diagnostics,
179
- newTransformCompilerDiagnostic(filename, "typia.transform", err.Error()),
180
- )
181
- continue
182
- }
183
- output.TypeScript[sourceFileKey(cwd, filename)] = cleanupTypeScriptTransformText(transformed)
184
- }
185
- if err := json.NewEncoder(stdout).Encode(output); err != nil {
186
- fmt.Fprintf(stderr, "ttsc-nestia transform: encode output: %v\n", err)
187
- return 3
188
- }
189
- if len(output.Diagnostics) > 0 {
190
- return 3
191
- }
192
- return 0
193
- }
194
-
195
- type SourceRewrite struct {
196
- start int
197
- end int
198
- replacement string
199
- }
200
-
201
- func collectTypiaSourceRewrites(
202
- prog *driver.Program,
203
- cwd string,
204
- onlyFile string,
205
- pluginOptions typiaadapter.PluginOptions,
206
- ) ([]SourceRewrite, []Diagnostic) {
207
- sites := collectNestiaTypiaCallSites(prog.SourceFiles(), prog.Checker)
208
- rewrites := []SourceRewrite{}
209
- diagnostics := []Diagnostic{}
210
- for _, site := range sites {
211
- if filepath.ToSlash(site.FilePath) != filepath.ToSlash(onlyFile) {
212
- continue
213
- }
214
- if reason := typiaadapter.UnsupportedReason(site); reason != "" {
215
- diagnostics = append(diagnostics, NewDiagnostic(site, reason))
216
- continue
217
- }
218
- expr, handled, err := typiaadapter.EmitCallWithOptionsPreservingTypes(prog, site, pluginOptions)
219
- if !handled {
220
- diagnostics = append(diagnostics, NewDiagnostic(site, "method not covered"))
221
- continue
222
- }
223
- if err != nil {
224
- diagnostics = append(diagnostics, NewDiagnostic(site, err.Error()))
225
- continue
226
- }
227
- expr = parenthesizeTypiaReplacement(site, expr)
228
- node := site.Call.AsNode()
229
- rewrites = append(rewrites, SourceRewrite{
230
- start: node.Pos(),
231
- end: node.End(),
232
- replacement: expr,
233
- })
234
- _ = cwd
235
- }
236
- return rewrites, diagnostics
237
- }
238
-
239
- func collectTypiaSourceRewriteMap(
240
- prog *driver.Program,
241
- pluginOptions typiaadapter.PluginOptions,
242
- ) (map[string][]SourceRewrite, []Diagnostic) {
243
- sites := collectNestiaTypiaCallSites(prog.SourceFiles(), prog.Checker)
244
- rewrites := map[string][]SourceRewrite{}
245
- diagnostics := []Diagnostic{}
246
- for _, site := range sites {
247
- file := filepath.ToSlash(site.FilePath)
248
- if reason := typiaadapter.UnsupportedReason(site); reason != "" {
249
- diagnostics = append(diagnostics, NewDiagnostic(site, reason))
250
- continue
251
- }
252
- expr, handled, err := typiaadapter.EmitCallWithOptionsPreservingTypes(prog, site, pluginOptions)
253
- if !handled {
254
- diagnostics = append(diagnostics, NewDiagnostic(site, "method not covered"))
255
- continue
256
- }
257
- if err != nil {
258
- diagnostics = append(diagnostics, NewDiagnostic(site, err.Error()))
259
- continue
260
- }
261
- expr = parenthesizeTypiaReplacement(site, expr)
262
- node := site.Call.AsNode()
263
- rewrites[file] = append(rewrites[file], SourceRewrite{
264
- start: node.Pos(),
265
- end: node.End(),
266
- replacement: expr,
267
- })
268
- }
269
- return rewrites, diagnostics
270
- }
271
-
272
- func ApplySourceRewrites(source string, rewrites []SourceRewrite) (string, error) {
273
- sort.SliceStable(rewrites, func(i, j int) bool {
274
- return rewrites[i].start > rewrites[j].start
275
- })
276
- for i := 0; i < len(rewrites)-1; i++ {
277
- if rewrites[i+1].end > rewrites[i].start {
278
- return "", fmt.Errorf("overlapping rewrites: [%d,%d) vs [%d,%d)",
279
- rewrites[i+1].start, rewrites[i+1].end, rewrites[i].start, rewrites[i].end)
280
- }
281
- }
282
- output := source
283
- for _, rewrite := range rewrites {
284
- if rewrite.start < 0 || rewrite.end < rewrite.start || rewrite.end > len(output) {
285
- return "", fmt.Errorf("invalid rewrite range [%d,%d)", rewrite.start, rewrite.end)
286
- }
287
- output = output[:rewrite.start] + rewrite.replacement + output[rewrite.end:]
288
- }
289
- return output, nil
290
- }
291
-
292
- func SourceFileText(target any) (string, bool) {
293
- type sourceText interface {
294
- Text() string
295
- }
296
- file, ok := target.(sourceText)
297
- if !ok {
298
- return "", false
299
- }
300
- return file.Text(), true
301
- }
302
-
303
- // These patterns run once per transformed file in cleanupTypeScriptTransformText
304
- // / normalizeParenthesizedTypeAnnotations. Compiling them at package scope keeps
305
- // the per-file cost to a match instead of a recompile (the SDK `transform` path
306
- // runs this for every emitted source file).
307
- var (
308
- cleanupImportTypeBlockPattern = regexp.MustCompile(`(?m)^import type \{([^{}\n]+)\} from`)
309
- cleanupImportTypeLinePattern = regexp.MustCompile(`^import type \{\s*([^{}\n]+?)\s*\} from`)
310
- cleanupImportBlankLinePattern = regexp.MustCompile(`(?m)(^import [^\n]+;\n)\n+(const |let |var |export )`)
311
- cleanupInputIsParenPattern = regexp.MustCompile(`input is \(([A-Za-z_$][A-Za-z0-9_$.]*)\)`)
312
- cleanupCollapseBlankCallPattern = regexp.MustCompile(`\n\n([A-Za-z_$][A-Za-z0-9_$]*\([^;\n]*\);?)`)
313
- )
314
-
315
- func cleanupTypeScriptTransformText(text string) string {
316
- text = cleanupTransformedText(text)
317
- text = normalizeParenthesizedTypeAnnotations(text)
318
- text = cleanupImportTypeBlockPattern.ReplaceAllStringFunc(text, func(line string) string {
319
- return cleanupImportTypeLinePattern.ReplaceAllString(line, "import type { $1 } from")
320
- })
321
- text = cleanupImportBlankLinePattern.ReplaceAllString(text, "$1$2")
322
- text = strings.ReplaceAll(text, "=(() =>", "= (() =>")
323
- text = strings.ReplaceAll(text, ": (any) =>", ": any =>")
324
- text = strings.ReplaceAll(text, ": (boolean) =>", ": boolean =>")
325
- text = cleanupInputIsParenPattern.ReplaceAllString(text, "input is $1")
326
- text = strings.ReplaceAll(text, "return (success ? ", "return success ? ")
327
- text = strings.ReplaceAll(text, "}) as any;", "} as any;")
328
- text = strings.ReplaceAll(text, "(() => {\n const ", "(() => { const ")
329
- text = strings.ReplaceAll(text, "(() => {\n let ", "(() => { let ")
330
- text = strings.ReplaceAll(text, "(() => {\n return ", "(() => { return ")
331
- text = strings.ReplaceAll(text, ";\n const ", "; const ")
332
- text = strings.ReplaceAll(text, ";\n let ", "; let ")
333
- text = strings.ReplaceAll(text, ";\n return ", "; return ")
334
- text = strings.ReplaceAll(text, "\n };\n})()", "\n}; })()")
335
- text = strings.ReplaceAll(text, "\n });\n})()", "\n}); })()")
336
- text = strings.ReplaceAll(text, "\n }); let ", "\n}); let ")
337
- text = strings.ReplaceAll(text, ";\n})()", "; })()")
338
- text = strings.ReplaceAll(text, "\n ", "\n ")
339
- text = cleanupCollapseBlankCallPattern.ReplaceAllString(text, "\n$1")
340
- trimmed := strings.TrimRight(text, " \t\r\n")
341
- if strings.HasSuffix(trimmed, ")") && !strings.HasSuffix(trimmed, ";") {
342
- return trimmed + ";\n"
343
- }
344
- if text != "" && !strings.HasSuffix(text, "\n") {
345
- return text + "\n"
346
- }
347
- return text
348
- }
349
-
350
- var (
351
- normalizeParenArrowTypePattern = regexp.MustCompile(`: \(([A-Za-z_$][A-Za-z0-9_$.]*(<[^()\n;{}]*>)?)\)(\s*=>)`)
352
- normalizeParenNullishPattern = regexp.MustCompile(`\| \((null|undefined)\)`)
353
- )
354
-
355
- func normalizeParenthesizedTypeAnnotations(text string) string {
356
- text = normalizeParenArrowTypePattern.ReplaceAllString(text, ": $1$3")
357
- text = normalizeParenNullishPattern.ReplaceAllString(text, "| $1")
358
- return text
359
- }
360
-
361
- func sourceFileKey(cwd string, file string) string {
362
- rel, err := filepath.Rel(cwd, filepath.FromSlash(file))
363
- if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
364
- return filepath.ToSlash(file)
365
- }
366
- return filepath.ToSlash(rel)
367
- }
368
-
369
- func newTransformCompilerDiagnostic(
370
- file string,
371
- code string,
372
- message string,
373
- ) transformCompilerDiagnostic {
374
- var ptr *string
375
- if file != "" {
376
- normalized := filepath.ToSlash(file)
377
- ptr = &normalized
378
- }
379
- return transformCompilerDiagnostic{
380
- File: ptr,
381
- Category: "error",
382
- Code: code,
383
- MessageText: message,
384
- }
385
- }
386
-
387
- func transformDiagnosticToCompilerDiagnostic(
388
- diag Diagnostic,
389
- ) transformCompilerDiagnostic {
390
- var ptr *string
391
- if diag.File != "" {
392
- normalized := filepath.ToSlash(diag.File)
393
- ptr = &normalized
394
- }
395
- return transformCompilerDiagnostic{
396
- File: ptr,
397
- Category: "error",
398
- Code: diag.Code,
399
- Line: diag.Line,
400
- Character: diag.Column,
401
- MessageText: diag.Message,
402
- }
403
- }
1
+ package transform
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "flag"
7
+ "fmt"
8
+ "os"
9
+ "path/filepath"
10
+ "regexp"
11
+ "sort"
12
+ "strings"
13
+
14
+ shimast "github.com/microsoft/typescript-go/shim/ast"
15
+ shimprinter "github.com/microsoft/typescript-go/shim/printer"
16
+ "github.com/samchon/nestia/packages/core/native/plugin"
17
+ "github.com/samchon/ttsc/packages/ttsc/driver"
18
+ )
19
+
20
+ type transformProjectOutput struct {
21
+ Diagnostics []transformCompilerDiagnostic `json:"diagnostics,omitempty"`
22
+ TypeScript map[string]string `json:"typescript"`
23
+ }
24
+
25
+ type transformCompilerDiagnostic struct {
26
+ File *string `json:"file"`
27
+ Category string `json:"category"`
28
+ Code string `json:"code"`
29
+ Line int `json:"line,omitempty"`
30
+ Character int `json:"character,omitempty"`
31
+ MessageText string `json:"messageText"`
32
+ }
33
+
34
+ func runTransform(args []string) int {
35
+ fs := flag.NewFlagSet("transform", flag.ContinueOnError)
36
+ fs.SetOutput(stderr)
37
+ file := fs.String("file", "", "absolute or cwd-relative path of the .ts file to transform")
38
+ tsconfigPath := fs.String("tsconfig", "tsconfig.json", "tsconfig.json owning --file")
39
+ cwdOverride := fs.String("cwd", "", "override the working directory")
40
+ out := fs.String("out", "", "write output to PATH")
41
+ output := fs.String("output", "ts", "transform output kind: ts")
42
+ pluginsJSON := fs.String("plugins-json", "", "ordered ttsc plugin payload")
43
+ if err := fs.Parse(args); err != nil {
44
+ return 2
45
+ }
46
+ if *output != "ts" {
47
+ fmt.Fprintf(stderr, "ttsc-nestia transform: unknown --output value %q\n", *output)
48
+ return 2
49
+ }
50
+ plan, err := plugin.ParsePlan(*pluginsJSON)
51
+ if err != nil {
52
+ fmt.Fprintf(stderr, "ttsc-nestia transform: %v\n", err)
53
+ return 2
54
+ }
55
+
56
+ cwd, ok := resolveCWD("ttsc-nestia transform", *cwdOverride)
57
+ if !ok {
58
+ return 2
59
+ }
60
+ prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
61
+ ForceEmit: true,
62
+ })
63
+ if err != nil {
64
+ fmt.Fprintf(stderr, "ttsc-nestia transform: %v\n", err)
65
+ return 2
66
+ }
67
+ if len(diags) > 0 {
68
+ driver.WritePrettyDiagnostics(stderr, diags, cwd)
69
+ return 2
70
+ }
71
+ defer prog.Close()
72
+
73
+ // AST-integration source-to-source: typia's, core's, and any linked
74
+ // contributor's per-file node transformers run inside one shared EmitContext
75
+ // and the result SourceFile is printed back as TypeScript (no JS script
76
+ // transformers), mirroring typia's `transform` subcommand. Injected namespace
77
+ // imports stay as ES imports the caller can type-strip per file, so there is
78
+ // no text-splice RewriteSet.
79
+ transformDiags := []Diagnostic{}
80
+ addDiagnostic := func(diag Diagnostic) {
81
+ transformDiags = append(transformDiags, diag)
82
+ }
83
+ typiaTransform := nestiaTypiaNodeTransform(prog, readTypiaPluginOptions(cwd, *tsconfigPath), addDiagnostic)
84
+ coreTransform := nestiaCoreNodeTransform(prog, plan, addDiagnostic)
85
+ contributorTransforms, contributorDiags := collectContributorEmitTransforms(prog, plan)
86
+ if len(contributorDiags) > 0 {
87
+ WriteTypiaTransformDiagnostics(stderr, contributorDiags, cwd)
88
+ return 3
89
+ }
90
+ transforms := append([]driver.PluginTransform{typiaTransform, coreTransform}, contributorTransforms...)
91
+
92
+ if *file == "" {
93
+ if *out != "" {
94
+ fmt.Fprintln(stderr, "ttsc-nestia transform: --out requires --file")
95
+ return 2
96
+ }
97
+ return runTransformProject(prog, cwd, transforms, &transformDiags)
98
+ }
99
+
100
+ absFile := *file
101
+ if !filepath.IsAbs(absFile) {
102
+ absFile = filepath.Join(cwd, absFile)
103
+ }
104
+ absFile = filepath.ToSlash(absFile)
105
+ target := prog.SourceFile(absFile)
106
+ if target == nil {
107
+ fmt.Fprintf(stderr, "ttsc-nestia transform: source file is not in program: %s\n", absFile)
108
+ return 2
109
+ }
110
+ text := transformFileToTypeScript(prog, transforms, target)
111
+ if len(transformDiags) > 0 {
112
+ WriteTypiaTransformDiagnostics(stderr, transformDiags, cwd)
113
+ return 3
114
+ }
115
+ return writeSingleOutput(text, *out)
116
+ }
117
+
118
+ func runTransformProject(
119
+ prog *driver.Program,
120
+ cwd string,
121
+ transforms []driver.PluginTransform,
122
+ transformDiags *[]Diagnostic,
123
+ ) int {
124
+ output := transformProjectOutput{
125
+ Diagnostics: []transformCompilerDiagnostic{},
126
+ TypeScript: map[string]string{},
127
+ }
128
+ for _, sf := range prog.SourceFiles() {
129
+ if sf.IsDeclarationFile {
130
+ continue
131
+ }
132
+ key := sourceFileKey(cwd, filepath.ToSlash(sf.FileName()))
133
+ if filepath.IsAbs(key) || key == ".." || strings.HasPrefix(key, "../") {
134
+ continue
135
+ }
136
+ output.TypeScript[key] = transformFileToTypeScript(prog, transforms, sf)
137
+ }
138
+ for _, diag := range *transformDiags {
139
+ output.Diagnostics = append(output.Diagnostics, transformDiagnosticToCompilerDiagnostic(diag))
140
+ }
141
+ if err := json.NewEncoder(stdout).Encode(output); err != nil {
142
+ fmt.Fprintf(stderr, "ttsc-nestia transform: encode output: %v\n", err)
143
+ return 3
144
+ }
145
+ if len(output.Diagnostics) > 0 {
146
+ return 3
147
+ }
148
+ return 0
149
+ }
150
+
151
+ // transformFileToTypeScript runs nestia's node transformers on one source file
152
+ // in a fresh EmitContext and prints the result as TypeScript. It deliberately
153
+ // skips the JS script transformers (type-erase, module-transform): the caller
154
+ // wants TS, so the namespace imports the transformers inject stay as ES imports.
155
+ func transformFileToTypeScript(
156
+ prog *driver.Program,
157
+ transforms []driver.PluginTransform,
158
+ sf *shimast.SourceFile,
159
+ ) string {
160
+ options := prog.TSProgram.Options()
161
+ ec := shimprinter.NewEmitContext()
162
+ result := sf
163
+ for _, t := range transforms {
164
+ if t == nil {
165
+ continue
166
+ }
167
+ if next := t(ec, result); next != nil {
168
+ result = next
169
+ }
170
+ }
171
+ shimast.SetParentInChildrenUnset(result.AsNode())
172
+ writer := shimprinter.NewTextWriter(options.NewLine.GetNewLineCharacter(), 0)
173
+ printer := shimprinter.NewPrinter(shimprinter.PrinterOptions{NewLine: options.NewLine}, shimprinter.PrintHandlers{}, ec)
174
+ printer.Write(result.AsNode(), result, writer, nil)
175
+ return writer.String()
176
+ }
177
+
178
+ func writeSingleOutput(text, outPath string) int {
179
+ if outPath == "" {
180
+ if _, err := bytes.NewReader([]byte(text)).WriteTo(stdout); err != nil {
181
+ fmt.Fprintf(stderr, "ttsc-nestia transform: write stdout: %v\n", err)
182
+ return 3
183
+ }
184
+ return 0
185
+ }
186
+ if dir := filepath.Dir(outPath); dir != "" {
187
+ if err := os.MkdirAll(dir, 0o755); err != nil {
188
+ fmt.Fprintf(stderr, "ttsc-nestia transform: mkdir: %v\n", err)
189
+ return 3
190
+ }
191
+ }
192
+ if err := os.WriteFile(outPath, []byte(text), 0o644); err != nil {
193
+ fmt.Fprintf(stderr, "ttsc-nestia transform: write %s: %v\n", outPath, err)
194
+ return 3
195
+ }
196
+ return 0
197
+ }
198
+
199
+ type SourceRewrite struct {
200
+ start int
201
+ end int
202
+ replacement string
203
+ }
204
+
205
+ func ApplySourceRewrites(source string, rewrites []SourceRewrite) (string, error) {
206
+ sort.SliceStable(rewrites, func(i, j int) bool {
207
+ return rewrites[i].start > rewrites[j].start
208
+ })
209
+ for i := 0; i < len(rewrites)-1; i++ {
210
+ if rewrites[i+1].end > rewrites[i].start {
211
+ return "", fmt.Errorf("overlapping rewrites: [%d,%d) vs [%d,%d)",
212
+ rewrites[i+1].start, rewrites[i+1].end, rewrites[i].start, rewrites[i].end)
213
+ }
214
+ }
215
+ output := source
216
+ for _, rewrite := range rewrites {
217
+ if rewrite.start < 0 || rewrite.end < rewrite.start || rewrite.end > len(output) {
218
+ return "", fmt.Errorf("invalid rewrite range [%d,%d)", rewrite.start, rewrite.end)
219
+ }
220
+ output = output[:rewrite.start] + rewrite.replacement + output[rewrite.end:]
221
+ }
222
+ return output, nil
223
+ }
224
+
225
+ func SourceFileText(target any) (string, bool) {
226
+ type sourceText interface {
227
+ Text() string
228
+ }
229
+ file, ok := target.(sourceText)
230
+ if !ok {
231
+ return "", false
232
+ }
233
+ return file.Text(), true
234
+ }
235
+
236
+ // These patterns run once per transformed file in cleanupTypeScriptTransformText
237
+ // / normalizeParenthesizedTypeAnnotations. Compiling them at package scope keeps
238
+ // the per-file cost to a match instead of a recompile (the SDK `transform` path
239
+ // runs this for every emitted source file).
240
+ var (
241
+ cleanupImportTypeBlockPattern = regexp.MustCompile(`(?m)^import type \{([^{}\n]+)\} from`)
242
+ cleanupImportTypeLinePattern = regexp.MustCompile(`^import type \{\s*([^{}\n]+?)\s*\} from`)
243
+ cleanupImportBlankLinePattern = regexp.MustCompile(`(?m)(^import [^\n]+;\n)\n+(const |let |var |export )`)
244
+ cleanupInputIsParenPattern = regexp.MustCompile(`input is \(([A-Za-z_$][A-Za-z0-9_$.]*)\)`)
245
+ cleanupCollapseBlankCallPattern = regexp.MustCompile(`\n\n([A-Za-z_$][A-Za-z0-9_$]*\([^;\n]*\);?)`)
246
+ )
247
+
248
+ func cleanupTypeScriptTransformText(text string) string {
249
+ text = cleanupTransformedText(text)
250
+ text = normalizeParenthesizedTypeAnnotations(text)
251
+ text = cleanupImportTypeBlockPattern.ReplaceAllStringFunc(text, func(line string) string {
252
+ return cleanupImportTypeLinePattern.ReplaceAllString(line, "import type { $1 } from")
253
+ })
254
+ text = cleanupImportBlankLinePattern.ReplaceAllString(text, "$1$2")
255
+ text = strings.ReplaceAll(text, "=(() =>", "= (() =>")
256
+ text = strings.ReplaceAll(text, ": (any) =>", ": any =>")
257
+ text = strings.ReplaceAll(text, ": (boolean) =>", ": boolean =>")
258
+ text = cleanupInputIsParenPattern.ReplaceAllString(text, "input is $1")
259
+ text = strings.ReplaceAll(text, "return (success ? ", "return success ? ")
260
+ text = strings.ReplaceAll(text, "}) as any;", "} as any;")
261
+ text = strings.ReplaceAll(text, "(() => {\n const ", "(() => { const ")
262
+ text = strings.ReplaceAll(text, "(() => {\n let ", "(() => { let ")
263
+ text = strings.ReplaceAll(text, "(() => {\n return ", "(() => { return ")
264
+ text = strings.ReplaceAll(text, ";\n const ", "; const ")
265
+ text = strings.ReplaceAll(text, ";\n let ", "; let ")
266
+ text = strings.ReplaceAll(text, ";\n return ", "; return ")
267
+ text = strings.ReplaceAll(text, "\n };\n})()", "\n}; })()")
268
+ text = strings.ReplaceAll(text, "\n });\n})()", "\n}); })()")
269
+ text = strings.ReplaceAll(text, "\n }); let ", "\n}); let ")
270
+ text = strings.ReplaceAll(text, ";\n})()", "; })()")
271
+ text = strings.ReplaceAll(text, "\n ", "\n ")
272
+ text = cleanupCollapseBlankCallPattern.ReplaceAllString(text, "\n$1")
273
+ trimmed := strings.TrimRight(text, " \t\r\n")
274
+ if strings.HasSuffix(trimmed, ")") && !strings.HasSuffix(trimmed, ";") {
275
+ return trimmed + ";\n"
276
+ }
277
+ if text != "" && !strings.HasSuffix(text, "\n") {
278
+ return text + "\n"
279
+ }
280
+ return text
281
+ }
282
+
283
+ var (
284
+ normalizeParenArrowTypePattern = regexp.MustCompile(`: \(([A-Za-z_$][A-Za-z0-9_$.]*(<[^()\n;{}]*>)?)\)(\s*=>)`)
285
+ normalizeParenNullishPattern = regexp.MustCompile(`\| \((null|undefined)\)`)
286
+ )
287
+
288
+ func normalizeParenthesizedTypeAnnotations(text string) string {
289
+ text = normalizeParenArrowTypePattern.ReplaceAllString(text, ": $1$3")
290
+ text = normalizeParenNullishPattern.ReplaceAllString(text, "| $1")
291
+ return text
292
+ }
293
+
294
+ func sourceFileKey(cwd string, file string) string {
295
+ rel, err := filepath.Rel(cwd, filepath.FromSlash(file))
296
+ if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
297
+ return filepath.ToSlash(file)
298
+ }
299
+ return filepath.ToSlash(rel)
300
+ }
301
+
302
+ func newTransformCompilerDiagnostic(
303
+ file string,
304
+ code string,
305
+ message string,
306
+ ) transformCompilerDiagnostic {
307
+ var ptr *string
308
+ if file != "" {
309
+ normalized := filepath.ToSlash(file)
310
+ ptr = &normalized
311
+ }
312
+ return transformCompilerDiagnostic{
313
+ File: ptr,
314
+ Category: "error",
315
+ Code: code,
316
+ MessageText: message,
317
+ }
318
+ }
319
+
320
+ func transformDiagnosticToCompilerDiagnostic(
321
+ diag Diagnostic,
322
+ ) transformCompilerDiagnostic {
323
+ var ptr *string
324
+ if diag.File != "" {
325
+ normalized := filepath.ToSlash(diag.File)
326
+ ptr = &normalized
327
+ }
328
+ return transformCompilerDiagnostic{
329
+ File: ptr,
330
+ Category: "error",
331
+ Code: diag.Code,
332
+ Line: diag.Line,
333
+ Character: diag.Column,
334
+ MessageText: diag.Message,
335
+ }
336
+ }