@nestia/core 11.2.1 → 12.0.0-dev.20260521.1

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 (178) hide show
  1. package/MIGRATION.md +169 -0
  2. package/lib/adaptors/WebSocketAdaptor.js +7 -3
  3. package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
  4. package/lib/decorators/DynamicModule.js.map +1 -1
  5. package/lib/decorators/EncryptedBody.js.map +1 -1
  6. package/lib/decorators/EncryptedController.js.map +1 -1
  7. package/lib/decorators/EncryptedModule.js.map +1 -1
  8. package/lib/decorators/EncryptedRoute.js.map +1 -1
  9. package/lib/decorators/HumanRoute.js.map +1 -1
  10. package/lib/decorators/NoTransformConfigurationError.js +5 -2
  11. package/lib/decorators/NoTransformConfigurationError.js.map +1 -1
  12. package/lib/decorators/PlainBody.js.map +1 -1
  13. package/lib/decorators/SwaggerCustomizer.js.map +1 -1
  14. package/lib/decorators/SwaggerExample.js.map +1 -1
  15. package/lib/decorators/TypedBody.js.map +1 -1
  16. package/lib/decorators/TypedException.js.map +1 -1
  17. package/lib/decorators/TypedFormData.js.map +1 -1
  18. package/lib/decorators/TypedHeaders.js.map +1 -1
  19. package/lib/decorators/TypedParam.js +5 -2
  20. package/lib/decorators/TypedParam.js.map +1 -1
  21. package/lib/decorators/TypedQuery.js.map +1 -1
  22. package/lib/decorators/TypedRoute.js.map +1 -1
  23. package/lib/decorators/WebSocketRoute.js.map +1 -1
  24. package/lib/decorators/doNotThrowTransformError.js.map +1 -1
  25. package/lib/decorators/internal/get_path_and_querify.js +5 -2
  26. package/lib/decorators/internal/get_path_and_querify.js.map +1 -1
  27. package/lib/decorators/internal/get_path_and_stringify.js +5 -2
  28. package/lib/decorators/internal/get_path_and_stringify.js.map +1 -1
  29. package/lib/decorators/internal/get_text_body.js.map +1 -1
  30. package/lib/decorators/internal/headers_to_object.js.map +1 -1
  31. package/lib/decorators/internal/is_request_body_undefined.js.map +1 -1
  32. package/lib/decorators/internal/load_controller.js +48 -16
  33. package/lib/decorators/internal/load_controller.js.map +1 -1
  34. package/lib/decorators/internal/route_error.js +3 -3
  35. package/lib/decorators/internal/route_error.js.map +1 -1
  36. package/lib/decorators/internal/validate_request_body.js +5 -2
  37. package/lib/decorators/internal/validate_request_body.js.map +1 -1
  38. package/lib/decorators/internal/validate_request_form_data.js +5 -2
  39. package/lib/decorators/internal/validate_request_form_data.js.map +1 -1
  40. package/lib/decorators/internal/validate_request_headers.js +5 -2
  41. package/lib/decorators/internal/validate_request_headers.js.map +1 -1
  42. package/lib/decorators/internal/validate_request_query.js +30 -4
  43. package/lib/decorators/internal/validate_request_query.js.map +1 -1
  44. package/lib/index.js.map +1 -1
  45. package/lib/transform.d.ts +10 -5
  46. package/lib/transform.js +64 -28
  47. package/lib/transform.js.map +1 -1
  48. package/lib/utils/ArrayUtil.js.map +1 -1
  49. package/lib/utils/ExceptionManager.js.map +1 -1
  50. package/lib/utils/Singleton.js.map +1 -1
  51. package/lib/utils/SourceFinder.js.map +1 -1
  52. package/lib/utils/VersioningStrategy.js.map +1 -1
  53. package/native/cmd/ttsc-nestia/main.go +11 -0
  54. package/native/go.mod +32 -0
  55. package/native/go.sum +54 -0
  56. package/native/plugin/plan.go +102 -0
  57. package/native/transform/ast.go +32 -0
  58. package/native/transform/build.go +413 -0
  59. package/native/transform/cleanup.go +408 -0
  60. package/native/transform/cleanup_test.go +76 -0
  61. package/native/transform/commonjs_import_alias_test.go +49 -0
  62. package/native/transform/core_dispatch_test.go +127 -0
  63. package/native/transform/core_querify.go +227 -0
  64. package/native/transform/core_transform.go +1713 -0
  65. package/native/transform/core_websocket.go +115 -0
  66. package/native/transform/exports.go +13 -0
  67. package/native/transform/path_rewrite.go +285 -0
  68. package/native/transform/path_rewrite_test.go +243 -0
  69. package/native/transform/printer.go +244 -0
  70. package/native/transform/rewrite.go +662 -0
  71. package/native/transform/rewrite_test.go +118 -0
  72. package/native/transform/rewrite_unique_base_test.go +48 -0
  73. package/native/transform/run.go +72 -0
  74. package/native/transform/transform.go +376 -0
  75. package/native/transform/typia_fast.go +326 -0
  76. package/native/transform/typia_replacement.go +24 -0
  77. package/native/transform.cjs +43 -0
  78. package/package.json +28 -22
  79. package/src/decorators/NoTransformConfigurationError.ts +5 -2
  80. package/src/decorators/internal/load_controller.ts +50 -19
  81. package/src/decorators/internal/validate_request_query.ts +21 -2
  82. package/src/transform.ts +101 -35
  83. package/lib/decorators/internal/NoTransformConfigureError.d.ts +0 -1
  84. package/lib/decorators/internal/NoTransformConfigureError.js +0 -7
  85. package/lib/decorators/internal/NoTransformConfigureError.js.map +0 -1
  86. package/lib/options/INestiaTransformOptions.d.ts +0 -13
  87. package/lib/options/INestiaTransformOptions.js +0 -3
  88. package/lib/options/INestiaTransformOptions.js.map +0 -1
  89. package/lib/options/INestiaTransformProject.d.ts +0 -5
  90. package/lib/options/INestiaTransformProject.js +0 -3
  91. package/lib/options/INestiaTransformProject.js.map +0 -1
  92. package/lib/programmers/PlainBodyProgrammer.d.ts +0 -9
  93. package/lib/programmers/PlainBodyProgrammer.js +0 -58
  94. package/lib/programmers/PlainBodyProgrammer.js.map +0 -1
  95. package/lib/programmers/TypedBodyProgrammer.d.ts +0 -9
  96. package/lib/programmers/TypedBodyProgrammer.js +0 -94
  97. package/lib/programmers/TypedBodyProgrammer.js.map +0 -1
  98. package/lib/programmers/TypedFormDataBodyProgrammer.d.ts +0 -9
  99. package/lib/programmers/TypedFormDataBodyProgrammer.js +0 -65
  100. package/lib/programmers/TypedFormDataBodyProgrammer.js.map +0 -1
  101. package/lib/programmers/TypedHeadersProgrammer.d.ts +0 -9
  102. package/lib/programmers/TypedHeadersProgrammer.js +0 -38
  103. package/lib/programmers/TypedHeadersProgrammer.js.map +0 -1
  104. package/lib/programmers/TypedParamProgrammer.d.ts +0 -10
  105. package/lib/programmers/TypedParamProgrammer.js +0 -32
  106. package/lib/programmers/TypedParamProgrammer.js.map +0 -1
  107. package/lib/programmers/TypedQueryBodyProgrammer.d.ts +0 -9
  108. package/lib/programmers/TypedQueryBodyProgrammer.js +0 -74
  109. package/lib/programmers/TypedQueryBodyProgrammer.js.map +0 -1
  110. package/lib/programmers/TypedQueryProgrammer.d.ts +0 -9
  111. package/lib/programmers/TypedQueryProgrammer.js +0 -75
  112. package/lib/programmers/TypedQueryProgrammer.js.map +0 -1
  113. package/lib/programmers/TypedQueryRouteProgrammer.d.ts +0 -9
  114. package/lib/programmers/TypedQueryRouteProgrammer.js +0 -75
  115. package/lib/programmers/TypedQueryRouteProgrammer.js.map +0 -1
  116. package/lib/programmers/TypedRouteProgrammer.d.ts +0 -9
  117. package/lib/programmers/TypedRouteProgrammer.js +0 -79
  118. package/lib/programmers/TypedRouteProgrammer.js.map +0 -1
  119. package/lib/programmers/http/HttpAssertQuerifyProgrammer.d.ts +0 -9
  120. package/lib/programmers/http/HttpAssertQuerifyProgrammer.js +0 -39
  121. package/lib/programmers/http/HttpAssertQuerifyProgrammer.js.map +0 -1
  122. package/lib/programmers/http/HttpIsQuerifyProgrammer.d.ts +0 -9
  123. package/lib/programmers/http/HttpIsQuerifyProgrammer.js +0 -36
  124. package/lib/programmers/http/HttpIsQuerifyProgrammer.js.map +0 -1
  125. package/lib/programmers/http/HttpQuerifyProgrammer.d.ts +0 -9
  126. package/lib/programmers/http/HttpQuerifyProgrammer.js +0 -50
  127. package/lib/programmers/http/HttpQuerifyProgrammer.js.map +0 -1
  128. package/lib/programmers/http/HttpValidateQuerifyProgrammer.d.ts +0 -9
  129. package/lib/programmers/http/HttpValidateQuerifyProgrammer.js +0 -40
  130. package/lib/programmers/http/HttpValidateQuerifyProgrammer.js.map +0 -1
  131. package/lib/programmers/internal/CoreMetadataUtil.d.ts +0 -5
  132. package/lib/programmers/internal/CoreMetadataUtil.js +0 -19
  133. package/lib/programmers/internal/CoreMetadataUtil.js.map +0 -1
  134. package/lib/transformers/FileTransformer.d.ts +0 -5
  135. package/lib/transformers/FileTransformer.js +0 -80
  136. package/lib/transformers/FileTransformer.js.map +0 -1
  137. package/lib/transformers/MethodTransformer.d.ts +0 -8
  138. package/lib/transformers/MethodTransformer.js +0 -58
  139. package/lib/transformers/MethodTransformer.js.map +0 -1
  140. package/lib/transformers/NodeTransformer.d.ts +0 -8
  141. package/lib/transformers/NodeTransformer.js +0 -24
  142. package/lib/transformers/NodeTransformer.js.map +0 -1
  143. package/lib/transformers/ParameterDecoratorTransformer.d.ts +0 -9
  144. package/lib/transformers/ParameterDecoratorTransformer.js +0 -104
  145. package/lib/transformers/ParameterDecoratorTransformer.js.map +0 -1
  146. package/lib/transformers/ParameterTransformer.d.ts +0 -8
  147. package/lib/transformers/ParameterTransformer.js +0 -37
  148. package/lib/transformers/ParameterTransformer.js.map +0 -1
  149. package/lib/transformers/TypedRouteTransformer.d.ts +0 -9
  150. package/lib/transformers/TypedRouteTransformer.js +0 -68
  151. package/lib/transformers/TypedRouteTransformer.js.map +0 -1
  152. package/lib/transformers/WebSocketRouteTransformer.d.ts +0 -9
  153. package/lib/transformers/WebSocketRouteTransformer.js +0 -72
  154. package/lib/transformers/WebSocketRouteTransformer.js.map +0 -1
  155. package/src/decorators/internal/NoTransformConfigureError.ts +0 -2
  156. package/src/options/INestiaTransformOptions.ts +0 -34
  157. package/src/options/INestiaTransformProject.ts +0 -10
  158. package/src/programmers/PlainBodyProgrammer.ts +0 -72
  159. package/src/programmers/TypedBodyProgrammer.ts +0 -148
  160. package/src/programmers/TypedFormDataBodyProgrammer.ts +0 -118
  161. package/src/programmers/TypedHeadersProgrammer.ts +0 -65
  162. package/src/programmers/TypedParamProgrammer.ts +0 -33
  163. package/src/programmers/TypedQueryBodyProgrammer.ts +0 -113
  164. package/src/programmers/TypedQueryProgrammer.ts +0 -115
  165. package/src/programmers/TypedQueryRouteProgrammer.ts +0 -107
  166. package/src/programmers/TypedRouteProgrammer.ts +0 -103
  167. package/src/programmers/http/HttpAssertQuerifyProgrammer.ts +0 -74
  168. package/src/programmers/http/HttpIsQuerifyProgrammer.ts +0 -77
  169. package/src/programmers/http/HttpQuerifyProgrammer.ts +0 -110
  170. package/src/programmers/http/HttpValidateQuerifyProgrammer.ts +0 -78
  171. package/src/programmers/internal/CoreMetadataUtil.ts +0 -21
  172. package/src/transformers/FileTransformer.ts +0 -109
  173. package/src/transformers/MethodTransformer.ts +0 -103
  174. package/src/transformers/NodeTransformer.ts +0 -23
  175. package/src/transformers/ParameterDecoratorTransformer.ts +0 -143
  176. package/src/transformers/ParameterTransformer.ts +0 -57
  177. package/src/transformers/TypedRouteTransformer.ts +0 -85
  178. package/src/transformers/WebSocketRouteTransformer.ts +0 -120
@@ -0,0 +1,118 @@
1
+ package transform
2
+
3
+ import "testing"
4
+
5
+ // Verifies OutputMatchesSourceStem accepts virtual-fs same-stem mirrors and
6
+ // rejects single-segment suffix collisions across the source tree.
7
+ //
8
+ // Regression: a top-level `src/index.ts` produced a one-segment relative
9
+ // ("index") that the HasSuffix fallback matched against every output ending
10
+ // in `/index.js`. The native rewrite scan then trawled unrelated files for
11
+ // typia call sites that only existed in the root entry, raising
12
+ // "could not locate typia.reflect.schemas(...) call".
13
+ //
14
+ // 1. Mirror a src/index virtual fs — must match.
15
+ // 2. Compare top-level src/index against nested src/api/health/index — must reject.
16
+ // 3. Cover the package src->lib mapping and an unrelated stem pair.
17
+ func TestOutputMatchesSourceStem(t *testing.T) {
18
+ cases := []struct {
19
+ name string
20
+ output string
21
+ source string
22
+ want bool
23
+ }{
24
+ {
25
+ name: "virtual fs mirrors src tree",
26
+ output: "/cache/.ttsc/project/1/fs/posix/repo/tests/foo/src/index",
27
+ source: "/repo/tests/foo/src/index",
28
+ want: true,
29
+ },
30
+ {
31
+ name: "top-level src/index does not match nested src/api/health/index",
32
+ output: "/cache/.ttsc/project/1/fs/posix/repo/tests/foo/src/api/functional/health/index",
33
+ source: "/repo/tests/foo/src/index",
34
+ want: false,
35
+ },
36
+ {
37
+ name: "src/api source matches nested api output",
38
+ output: "/cache/.ttsc/project/1/fs/posix/repo/tests/foo/src/api/functional/health/index",
39
+ source: "/repo/tests/foo/src/api/functional/health/index",
40
+ want: true,
41
+ },
42
+ {
43
+ name: "package src maps to lib output",
44
+ output: "/repo/packages/core/lib/decorators/TypedBody",
45
+ source: "/repo/packages/core/src/decorators/TypedBody",
46
+ want: true,
47
+ },
48
+ {
49
+ name: "unrelated stems",
50
+ output: "/repo/packages/core/lib/decorators/TypedBody",
51
+ source: "/repo/packages/sdk/src/generates/Foo",
52
+ want: false,
53
+ },
54
+ {
55
+ name: "top-level src/index does not match nested src/api/index",
56
+ output: "/cache/.ttsc/project/1/fs/posix/repo/tests/foo/src/api/index",
57
+ source: "/repo/tests/foo/src/index",
58
+ want: false,
59
+ },
60
+ }
61
+ for _, tc := range cases {
62
+ got := OutputMatchesSourceStem(tc.output, tc.source)
63
+ if got != tc.want {
64
+ t.Errorf("%s: OutputMatchesSourceStem(%q, %q) = %v, want %v",
65
+ tc.name, tc.output, tc.source, got, tc.want)
66
+ }
67
+ }
68
+ }
69
+
70
+ func TestCountNativeArgumentsBasic(t *testing.T) {
71
+ cases := []struct {
72
+ name string
73
+ in string
74
+ want int
75
+ }{
76
+ {"empty", "", 0},
77
+ {"single", "x", 1},
78
+ {"two simple", "a, b", 2},
79
+ {"three with object", "a, { b: 1, c: 2 }, d", 3},
80
+ {"comma in string", `"a, b", c`, 2},
81
+ {"comma in single quote", `'a, b', c, d`, 3},
82
+ {"escaped quote", `"a\"b", c`, 2},
83
+ {"comma in template", "`a,b`, c", 2},
84
+ {"nested call", "foo(a, b), c", 2},
85
+ {"nested array", "[1, 2, 3], y", 2},
86
+ }
87
+ for _, tc := range cases {
88
+ got := countNativeArguments(tc.in)
89
+ if got != tc.want {
90
+ t.Errorf("%s: countNativeArguments(%q) = %d, want %d", tc.name, tc.in, got, tc.want)
91
+ }
92
+ }
93
+ }
94
+
95
+ // Regression: template-literal `${...}` substitutions contain real expressions
96
+ // — any '(' / ')' / ',' inside a substitution must be treated as expression
97
+ // tokens, not template-string content. Prior implementation skipped the entire
98
+ // backtick range as if it were a flat string and miscounted arguments when a
99
+ // substitution contained quote-shaped characters or nested templates.
100
+ func TestCountNativeArgumentsTemplateSubstitution(t *testing.T) {
101
+ cases := []struct {
102
+ name string
103
+ in string
104
+ want int
105
+ }{
106
+ {"quote-inside-substitution", "`x${\")\"}y`, foo", 2},
107
+ {"comma-inside-substitution-grouped", "`x${(1, 2)}y`, foo", 2},
108
+ {"nested-template", "`a${`b${1}c`}d`, foo", 2},
109
+ {"substitution-with-object", "`x${{ a: 1, b: 2 }}y`, foo", 2},
110
+ {"call-inside-substitution", "`x${ f(1, 2) }y`, foo", 2},
111
+ }
112
+ for _, tc := range cases {
113
+ got := countNativeArguments(tc.in)
114
+ if got != tc.want {
115
+ t.Errorf("%s: countNativeArguments(%q) = %d, want %d", tc.name, tc.in, got, tc.want)
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,48 @@
1
+ package transform
2
+
3
+ import "testing"
4
+
5
+ func TestNativeRewriteSetFindSourceByUniqueBase(t *testing.T) {
6
+ set := newNativeRewriteSet()
7
+ set.Add(nativeRewrite{
8
+ FilePath: "/repo/tests/test-transform-options/src/validate.ts",
9
+ Method: "TypedBody",
10
+ })
11
+
12
+ got, ok := set.findSourceForOutput(
13
+ "/repo/tests/test-transform-options/lib/validate-assert/validate.js",
14
+ )
15
+ if !ok {
16
+ t.Fatal("expected unique basename fallback to match source")
17
+ }
18
+ if got != "/repo/tests/test-transform-options/src/validate.ts" {
19
+ t.Fatalf("unexpected source match: %q", got)
20
+ }
21
+ }
22
+
23
+ // Verifies the unique-basename fallback does not match a sibling output that
24
+ // lives inside the same `src/` tree as the registered source.
25
+ //
26
+ // Regression: the fallback used to match any output sharing a basename with
27
+ // a registered source, so `src/index.ts` silently absorbed rewrites destined
28
+ // for every `src/.../index.ts` neighbor produced by ttsc's virtual fs. The
29
+ // symptom was `could not locate typia.*(...)` on unrelated generated files.
30
+ //
31
+ // 1. Register one rewrite for `/repo/tests/foo/src/index.ts`.
32
+ // 2. Ask findSourceForOutput for a sibling output inside the same src tree.
33
+ // 3. Expect no match.
34
+ func TestNativeRewriteSetFindSourceByUniqueBaseSkipsSourceTreeSiblings(t *testing.T) {
35
+ set := newNativeRewriteSet()
36
+ set.Add(nativeRewrite{
37
+ FilePath: "/repo/tests/foo/src/index.ts",
38
+ Method: "schemas",
39
+ })
40
+
41
+ // Output sits inside the same src/ tree, not in lib/dist/bin/build — it
42
+ // must not absorb the rewrite registered for src/index.ts.
43
+ if got, ok := set.findSourceForOutput(
44
+ "/cache/.ttsc/project/1/fs/posix/repo/tests/foo/src/api/functional/health/index.js",
45
+ ); ok {
46
+ t.Fatalf("expected no match for source-tree sibling, got %q", got)
47
+ }
48
+ }
@@ -0,0 +1,72 @@
1
+ package transform
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "os"
7
+ "runtime/debug"
8
+ )
9
+
10
+ var (
11
+ stdout io.Writer = os.Stdout
12
+ stderr io.Writer = os.Stderr
13
+ )
14
+
15
+ // Run wraps run() in a panic recovery envelope so that any unexpected
16
+ // panic surfaces as a one-line transform-diagnostic on stderr instead of
17
+ // a multi-line raw Go stack trace. The diagnostic uses the same `<file> -
18
+ // error TS(code): message` shape as every other nestia / typia diagnostic;
19
+ // ttsc reads it via `error.stderr` (`RuntimeCompiler.compile` at
20
+ // `ConfigAnalyzer.ts:170-174`) rather than its structured-diagnostic regex,
21
+ // which is a pre-existing protocol shared by all `nestia.*` codes. The
22
+ // full stack is preserved behind NESTIA_NATIVE_DEBUG_STACK for triage.
23
+ //
24
+ // This is the `@nestia/core` plugin entry point — it performs the typia
25
+ // and `@nestia/core` decorator transforms only. The `@nestia/sdk` metadata
26
+ // transform lives in its own plugin (`packages/sdk/native`).
27
+ func Run(args []string) (code int) {
28
+ defer func() {
29
+ if exp := recover(); exp != nil {
30
+ diag := Diagnostic{
31
+ Code: "nestia.internal.panic",
32
+ Message: fmt.Sprintf("ttsc-nestia panicked: %v", exp),
33
+ }
34
+ WriteTypiaTransformDiagnostics(stderr, []Diagnostic{diag}, "")
35
+ if os.Getenv("NESTIA_NATIVE_DEBUG_STACK") != "" {
36
+ fmt.Fprintln(stderr, string(debug.Stack()))
37
+ }
38
+ code = 3
39
+ }
40
+ }()
41
+ return run(args)
42
+ }
43
+
44
+ func run(args []string) int {
45
+ if len(args) == 0 {
46
+ return runHelp(nil)
47
+ }
48
+ command := args[0]
49
+ rest := args[1:]
50
+ switch command {
51
+ case "build":
52
+ return runBuild(rest)
53
+ case "check":
54
+ return runCheck(rest)
55
+ case "transform":
56
+ return runTransform(rest)
57
+ case "version":
58
+ fmt.Fprintln(stdout, "ttsc-nestia 0.1.0")
59
+ return 0
60
+ case "help", "-h", "--help":
61
+ return runHelp(rest)
62
+ default:
63
+ fmt.Fprintf(stderr, "ttsc-nestia: unknown command %q\n", command)
64
+ return 2
65
+ }
66
+ }
67
+
68
+ func runHelp(args []string) int {
69
+ _ = args
70
+ fmt.Fprintln(stdout, "usage: ttsc-nestia <build|check|transform|version>")
71
+ return 0
72
+ }
@@ -0,0 +1,376 @@
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
+ source, ok := SourceFileText(target)
107
+ if !ok {
108
+ fmt.Fprintf(stderr, "ttsc-nestia transform: source text is unavailable for %s\n", absFile)
109
+ return 3
110
+ }
111
+ source, err = ApplySourceRewrites(source, rewrites)
112
+ if err != nil {
113
+ fmt.Fprintf(stderr, "ttsc-nestia transform: source rewrite: %v\n", err)
114
+ return 3
115
+ }
116
+ source = cleanupTypeScriptTransformText(source)
117
+ if *out == "" {
118
+ if _, err := bytes.NewBufferString(source).WriteTo(stdout); err != nil {
119
+ fmt.Fprintf(stderr, "ttsc-nestia transform: write stdout: %v\n", err)
120
+ return 3
121
+ }
122
+ return 0
123
+ }
124
+ if dir := filepath.Dir(*out); dir != "" {
125
+ if err := os.MkdirAll(dir, 0o755); err != nil {
126
+ fmt.Fprintf(stderr, "ttsc-nestia transform: mkdir: %v\n", err)
127
+ return 3
128
+ }
129
+ }
130
+ if err := os.WriteFile(*out, []byte(source), 0o644); err != nil {
131
+ fmt.Fprintf(stderr, "ttsc-nestia transform: write %s: %v\n", *out, err)
132
+ return 3
133
+ }
134
+ return 0
135
+ }
136
+
137
+ func runTransformProject(prog *driver.Program, cwd string, tsconfigPath string, plan plugin.Plan) int {
138
+ rewrites, diags := collectTypiaSourceRewriteMap(
139
+ prog,
140
+ readTypiaPluginOptions(cwd, tsconfigPath),
141
+ )
142
+ coreRewriteMap, coreDiags := collectNestiaCoreSourceRewriteMap(prog, plan, "")
143
+ for file, entries := range coreRewriteMap {
144
+ rewrites[file] = append(rewrites[file], entries...)
145
+ }
146
+ diags = append(diags, coreDiags...)
147
+ output := transformProjectOutput{
148
+ Diagnostics: make([]transformCompilerDiagnostic, 0, len(diags)),
149
+ TypeScript: map[string]string{},
150
+ }
151
+ for _, diag := range diags {
152
+ output.Diagnostics = append(output.Diagnostics, transformDiagnosticToCompilerDiagnostic(diag))
153
+ }
154
+ for _, file := range prog.SourceFiles() {
155
+ filename := filepath.ToSlash(file.FileName())
156
+ source, ok := SourceFileText(file)
157
+ if !ok {
158
+ output.Diagnostics = append(
159
+ output.Diagnostics,
160
+ newTransformCompilerDiagnostic(filename, "nestia.transform", "source text is unavailable"),
161
+ )
162
+ continue
163
+ }
164
+ transformed, err := ApplySourceRewrites(source, rewrites[filename])
165
+ if err != nil {
166
+ output.Diagnostics = append(
167
+ output.Diagnostics,
168
+ newTransformCompilerDiagnostic(filename, "typia.transform", err.Error()),
169
+ )
170
+ continue
171
+ }
172
+ output.TypeScript[sourceFileKey(cwd, filename)] = cleanupTypeScriptTransformText(transformed)
173
+ }
174
+ if err := json.NewEncoder(stdout).Encode(output); err != nil {
175
+ fmt.Fprintf(stderr, "ttsc-nestia transform: encode output: %v\n", err)
176
+ return 3
177
+ }
178
+ if len(output.Diagnostics) > 0 {
179
+ return 3
180
+ }
181
+ return 0
182
+ }
183
+
184
+ type SourceRewrite struct {
185
+ start int
186
+ end int
187
+ replacement string
188
+ }
189
+
190
+ func collectTypiaSourceRewrites(
191
+ prog *driver.Program,
192
+ cwd string,
193
+ onlyFile string,
194
+ pluginOptions typiaadapter.PluginOptions,
195
+ ) ([]SourceRewrite, []Diagnostic) {
196
+ sites := collectNestiaTypiaCallSites(prog.SourceFiles(), prog.Checker)
197
+ rewrites := []SourceRewrite{}
198
+ diagnostics := []Diagnostic{}
199
+ for _, site := range sites {
200
+ if filepath.ToSlash(site.FilePath) != filepath.ToSlash(onlyFile) {
201
+ continue
202
+ }
203
+ if reason := typiaadapter.UnsupportedReason(site); reason != "" {
204
+ diagnostics = append(diagnostics, NewDiagnostic(site, reason))
205
+ continue
206
+ }
207
+ expr, handled, err := typiaadapter.EmitCallWithOptionsPreservingTypes(prog, site, pluginOptions)
208
+ if !handled {
209
+ diagnostics = append(diagnostics, NewDiagnostic(site, "method not covered"))
210
+ continue
211
+ }
212
+ if err != nil {
213
+ diagnostics = append(diagnostics, NewDiagnostic(site, err.Error()))
214
+ continue
215
+ }
216
+ expr = parenthesizeTypiaReplacement(site, expr)
217
+ node := site.Call.AsNode()
218
+ rewrites = append(rewrites, SourceRewrite{
219
+ start: node.Pos(),
220
+ end: node.End(),
221
+ replacement: expr,
222
+ })
223
+ _ = cwd
224
+ }
225
+ return rewrites, diagnostics
226
+ }
227
+
228
+ func collectTypiaSourceRewriteMap(
229
+ prog *driver.Program,
230
+ pluginOptions typiaadapter.PluginOptions,
231
+ ) (map[string][]SourceRewrite, []Diagnostic) {
232
+ sites := collectNestiaTypiaCallSites(prog.SourceFiles(), prog.Checker)
233
+ rewrites := map[string][]SourceRewrite{}
234
+ diagnostics := []Diagnostic{}
235
+ for _, site := range sites {
236
+ file := filepath.ToSlash(site.FilePath)
237
+ if reason := typiaadapter.UnsupportedReason(site); reason != "" {
238
+ diagnostics = append(diagnostics, NewDiagnostic(site, reason))
239
+ continue
240
+ }
241
+ expr, handled, err := typiaadapter.EmitCallWithOptionsPreservingTypes(prog, site, pluginOptions)
242
+ if !handled {
243
+ diagnostics = append(diagnostics, NewDiagnostic(site, "method not covered"))
244
+ continue
245
+ }
246
+ if err != nil {
247
+ diagnostics = append(diagnostics, NewDiagnostic(site, err.Error()))
248
+ continue
249
+ }
250
+ expr = parenthesizeTypiaReplacement(site, expr)
251
+ node := site.Call.AsNode()
252
+ rewrites[file] = append(rewrites[file], SourceRewrite{
253
+ start: node.Pos(),
254
+ end: node.End(),
255
+ replacement: expr,
256
+ })
257
+ }
258
+ return rewrites, diagnostics
259
+ }
260
+
261
+ func ApplySourceRewrites(source string, rewrites []SourceRewrite) (string, error) {
262
+ sort.SliceStable(rewrites, func(i, j int) bool {
263
+ return rewrites[i].start > rewrites[j].start
264
+ })
265
+ for i := 0; i < len(rewrites)-1; i++ {
266
+ if rewrites[i+1].end > rewrites[i].start {
267
+ return "", fmt.Errorf("overlapping rewrites: [%d,%d) vs [%d,%d)",
268
+ rewrites[i+1].start, rewrites[i+1].end, rewrites[i].start, rewrites[i].end)
269
+ }
270
+ }
271
+ output := source
272
+ for _, rewrite := range rewrites {
273
+ if rewrite.start < 0 || rewrite.end < rewrite.start || rewrite.end > len(output) {
274
+ return "", fmt.Errorf("invalid rewrite range [%d,%d)", rewrite.start, rewrite.end)
275
+ }
276
+ output = output[:rewrite.start] + rewrite.replacement + output[rewrite.end:]
277
+ }
278
+ return output, nil
279
+ }
280
+
281
+ func SourceFileText(target any) (string, bool) {
282
+ type sourceText interface {
283
+ Text() string
284
+ }
285
+ file, ok := target.(sourceText)
286
+ if !ok {
287
+ return "", false
288
+ }
289
+ return file.Text(), true
290
+ }
291
+
292
+ func cleanupTypeScriptTransformText(text string) string {
293
+ text = cleanupTransformedText(text)
294
+ text = normalizeParenthesizedTypeAnnotations(text)
295
+ text = regexp.MustCompile(`(?m)^import type \{([^{}\n]+)\} from`).ReplaceAllStringFunc(text, func(line string) string {
296
+ return regexp.MustCompile(`^import type \{\s*([^{}\n]+?)\s*\} from`).ReplaceAllString(line, "import type { $1 } from")
297
+ })
298
+ text = regexp.MustCompile(`(?m)(^import [^\n]+;\n)\n+(const |let |var |export )`).ReplaceAllString(text, "$1$2")
299
+ text = strings.ReplaceAll(text, "=(() =>", "= (() =>")
300
+ text = strings.ReplaceAll(text, ": (any) =>", ": any =>")
301
+ text = strings.ReplaceAll(text, ": (boolean) =>", ": boolean =>")
302
+ text = regexp.MustCompile(`input is \(([A-Za-z_$][A-Za-z0-9_$.]*)\)`).ReplaceAllString(text, "input is $1")
303
+ text = strings.ReplaceAll(text, "return (success ? ", "return success ? ")
304
+ text = strings.ReplaceAll(text, "}) as any;", "} as any;")
305
+ text = strings.ReplaceAll(text, "(() => {\n const ", "(() => { const ")
306
+ text = strings.ReplaceAll(text, "(() => {\n let ", "(() => { let ")
307
+ text = strings.ReplaceAll(text, "(() => {\n return ", "(() => { return ")
308
+ text = strings.ReplaceAll(text, ";\n const ", "; const ")
309
+ text = strings.ReplaceAll(text, ";\n let ", "; let ")
310
+ text = strings.ReplaceAll(text, ";\n return ", "; return ")
311
+ text = strings.ReplaceAll(text, "\n };\n})()", "\n}; })()")
312
+ text = strings.ReplaceAll(text, "\n });\n})()", "\n}); })()")
313
+ text = strings.ReplaceAll(text, "\n }); let ", "\n}); let ")
314
+ text = strings.ReplaceAll(text, ";\n})()", "; })()")
315
+ text = strings.ReplaceAll(text, "\n ", "\n ")
316
+ text = regexp.MustCompile(`\n\n([A-Za-z_$][A-Za-z0-9_$]*\([^;\n]*\);?)`).ReplaceAllString(text, "\n$1")
317
+ trimmed := strings.TrimRight(text, " \t\r\n")
318
+ if strings.HasSuffix(trimmed, ")") && !strings.HasSuffix(trimmed, ";") {
319
+ return trimmed + ";\n"
320
+ }
321
+ if text != "" && !strings.HasSuffix(text, "\n") {
322
+ return text + "\n"
323
+ }
324
+ return text
325
+ }
326
+
327
+ func normalizeParenthesizedTypeAnnotations(text string) string {
328
+ typeAtom := `([A-Za-z_$][A-Za-z0-9_$.]*(<[^()\n;{}]*>)?)`
329
+ text = regexp.MustCompile(`: \(`+typeAtom+`\)(\s*=>)`).ReplaceAllString(text, ": $1$3")
330
+ text = regexp.MustCompile(`\| \((null|undefined)\)`).ReplaceAllString(text, "| $1")
331
+ return text
332
+ }
333
+
334
+ func sourceFileKey(cwd string, file string) string {
335
+ rel, err := filepath.Rel(cwd, filepath.FromSlash(file))
336
+ if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
337
+ return filepath.ToSlash(file)
338
+ }
339
+ return filepath.ToSlash(rel)
340
+ }
341
+
342
+ func newTransformCompilerDiagnostic(
343
+ file string,
344
+ code string,
345
+ message string,
346
+ ) transformCompilerDiagnostic {
347
+ var ptr *string
348
+ if file != "" {
349
+ normalized := filepath.ToSlash(file)
350
+ ptr = &normalized
351
+ }
352
+ return transformCompilerDiagnostic{
353
+ File: ptr,
354
+ Category: "error",
355
+ Code: code,
356
+ MessageText: message,
357
+ }
358
+ }
359
+
360
+ func transformDiagnosticToCompilerDiagnostic(
361
+ diag Diagnostic,
362
+ ) transformCompilerDiagnostic {
363
+ var ptr *string
364
+ if diag.File != "" {
365
+ normalized := filepath.ToSlash(diag.File)
366
+ ptr = &normalized
367
+ }
368
+ return transformCompilerDiagnostic{
369
+ File: ptr,
370
+ Category: "error",
371
+ Code: diag.Code,
372
+ Line: diag.Line,
373
+ Character: diag.Column,
374
+ MessageText: diag.Message,
375
+ }
376
+ }