@nestia/core 12.0.0-rc.2 → 12.0.0-rc.3

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 (80) hide show
  1. package/LICENSE +21 -21
  2. package/MIGRATION.md +138 -169
  3. package/lib/decorators/NoTransformConfigurationError.js +5 -5
  4. package/lib/decorators/NoTransformConfigurationError.js.map +1 -1
  5. package/native/cmd/ttsc-nestia/main.go +11 -11
  6. package/native/go.mod +32 -32
  7. package/native/go.sum +54 -54
  8. package/native/plugin/plan.go +102 -102
  9. package/native/transform/ast.go +32 -32
  10. package/native/transform/build.go +388 -380
  11. package/native/transform/cleanup.go +408 -408
  12. package/native/transform/contributor.go +97 -97
  13. package/native/transform/core_querify.go +231 -231
  14. package/native/transform/core_transform.go +1996 -1996
  15. package/native/transform/core_websocket.go +115 -115
  16. package/native/transform/exports.go +13 -13
  17. package/native/transform/mcp_transform.go +414 -414
  18. package/native/transform/node_transform.go +357 -357
  19. package/native/transform/path_rewrite.go +285 -285
  20. package/native/transform/printer.go +244 -244
  21. package/native/transform/rewrite.go +668 -668
  22. package/native/transform/run.go +73 -73
  23. package/native/transform/transform.go +336 -336
  24. package/native/transform/typia_fast.go +375 -352
  25. package/native/transform/typia_replacement.go +24 -24
  26. package/native/transform.cjs +43 -43
  27. package/package.json +9 -9
  28. package/src/adaptors/McpAdaptor.ts +276 -276
  29. package/src/adaptors/WebSocketAdaptor.ts +429 -429
  30. package/src/decorators/DynamicModule.ts +44 -44
  31. package/src/decorators/EncryptedBody.ts +97 -97
  32. package/src/decorators/EncryptedController.ts +40 -40
  33. package/src/decorators/EncryptedModule.ts +98 -98
  34. package/src/decorators/EncryptedRoute.ts +213 -213
  35. package/src/decorators/HumanRoute.ts +21 -21
  36. package/src/decorators/McpRoute.ts +154 -154
  37. package/src/decorators/NoTransformConfigurationError.ts +40 -40
  38. package/src/decorators/PlainBody.ts +76 -76
  39. package/src/decorators/SwaggerCustomizer.ts +97 -97
  40. package/src/decorators/SwaggerExample.ts +180 -180
  41. package/src/decorators/TypedBody.ts +57 -57
  42. package/src/decorators/TypedException.ts +147 -147
  43. package/src/decorators/TypedFormData.ts +187 -187
  44. package/src/decorators/TypedHeaders.ts +66 -66
  45. package/src/decorators/TypedParam.ts +77 -77
  46. package/src/decorators/TypedQuery.ts +234 -234
  47. package/src/decorators/TypedRoute.ts +198 -198
  48. package/src/decorators/WebSocketRoute.ts +242 -242
  49. package/src/decorators/doNotThrowTransformError.ts +5 -5
  50. package/src/decorators/internal/EncryptedConstant.ts +2 -2
  51. package/src/decorators/internal/IMcpRouteReflect.ts +40 -40
  52. package/src/decorators/internal/IWebSocketRouteReflect.ts +23 -23
  53. package/src/decorators/internal/get_path_and_querify.ts +94 -94
  54. package/src/decorators/internal/get_path_and_stringify.ts +110 -110
  55. package/src/decorators/internal/get_text_body.ts +16 -16
  56. package/src/decorators/internal/headers_to_object.ts +11 -11
  57. package/src/decorators/internal/is_request_body_undefined.ts +12 -12
  58. package/src/decorators/internal/load_controller.ts +94 -94
  59. package/src/decorators/internal/route_error.ts +43 -43
  60. package/src/decorators/internal/validate_request_body.ts +64 -64
  61. package/src/decorators/internal/validate_request_form_data.ts +67 -67
  62. package/src/decorators/internal/validate_request_headers.ts +76 -76
  63. package/src/decorators/internal/validate_request_query.ts +83 -83
  64. package/src/index.ts +5 -5
  65. package/src/module.ts +25 -25
  66. package/src/options/IRequestBodyValidator.ts +20 -20
  67. package/src/options/IRequestFormDataProps.ts +27 -27
  68. package/src/options/IRequestHeadersValidator.ts +22 -22
  69. package/src/options/IRequestQueryValidator.ts +20 -20
  70. package/src/options/IResponseBodyQuerifier.ts +25 -25
  71. package/src/options/IResponseBodyStringifier.ts +30 -30
  72. package/src/transform.ts +26 -26
  73. package/src/typings/Creator.ts +3 -3
  74. package/src/typings/get-function-location.d.ts +7 -7
  75. package/src/utils/ArrayUtil.ts +7 -7
  76. package/src/utils/ExceptionManager.ts +115 -115
  77. package/src/utils/Singleton.ts +16 -16
  78. package/src/utils/SourceFinder.ts +54 -54
  79. package/src/utils/VersioningStrategy.ts +27 -27
  80. package/README.md +0 -93
@@ -1,336 +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
- 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
- }
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
+ }