@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,115 @@
1
+ package transform
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ shimast "github.com/microsoft/typescript-go/shim/ast"
8
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
9
+ "github.com/samchon/ttsc/packages/ttsc/driver"
10
+ )
11
+
12
+ func validateNestiaCoreWebSocketRoute(
13
+ prog *driver.Program,
14
+ context nestiaCoreFileContext,
15
+ method *shimast.Node,
16
+ call *shimast.CallExpression,
17
+ segments []string,
18
+ ) []Diagnostic {
19
+ if call == nil || len(segments) == 0 || segments[len(segments)-1] != "WebSocketRoute" {
20
+ _ = call
21
+ return nil
22
+ }
23
+ diagnostics := []Diagnostic{}
24
+ accepted := false
25
+ methodDecl := method.AsMethodDeclaration()
26
+ if methodDecl == nil || methodDecl.Parameters == nil {
27
+ return []Diagnostic{nestiaCoreWebSocketDiagnostic(context.file, method, "WebSocketRoute", fmt.Sprintf(
28
+ "method %q must have at least one parameter decorated by @WebSocketRoute.Acceptor().",
29
+ NodeName(method),
30
+ ))}
31
+ }
32
+ for _, param := range methodDecl.Parameters.Nodes {
33
+ category := nestiaCoreWebSocketParameterCategory(prog, param)
34
+ name := NodeName(param)
35
+ if category == "" {
36
+ diagnostics = append(diagnostics, nestiaCoreWebSocketDiagnostic(context.file, param, "WebSocketRoute", fmt.Sprintf(
37
+ "parameter %q is not decorated with nested function of WebSocketRoute module.",
38
+ name,
39
+ )))
40
+ continue
41
+ }
42
+ switch category {
43
+ case "Acceptor":
44
+ accepted = true
45
+ if strings.HasPrefix(nestiaCoreWebSocketParameterTypeName(param), "WebSocketAcceptor") == false {
46
+ diagnostics = append(diagnostics, nestiaCoreWebSocketDiagnostic(context.file, param, "WebSocketRoute", fmt.Sprintf(
47
+ "parameter %q must have WebSocketAcceptor<Header, Provider, Listener> type.",
48
+ name,
49
+ )))
50
+ }
51
+ case "Driver":
52
+ if strings.HasPrefix(nestiaCoreWebSocketParameterTypeName(param), "Driver") == false {
53
+ diagnostics = append(diagnostics, nestiaCoreWebSocketDiagnostic(context.file, param, "WebSocketRoute", fmt.Sprintf(
54
+ "parameter %q must have Driver<Listener> type.",
55
+ name,
56
+ )))
57
+ }
58
+ }
59
+ }
60
+ if accepted == false {
61
+ diagnostics = append(diagnostics, nestiaCoreWebSocketDiagnostic(context.file, method, "WebSocketRoute", fmt.Sprintf(
62
+ "method %q must have at least one parameter decorated by @WebSocketRoute.Acceptor().",
63
+ NodeName(method),
64
+ )))
65
+ }
66
+ return diagnostics
67
+ }
68
+
69
+ func nestiaCoreWebSocketParameterCategory(prog *driver.Program, param *shimast.Node) string {
70
+ if param == nil || len(param.Decorators()) != 1 {
71
+ return ""
72
+ }
73
+ call, segments, ok := nestiaCoreDecoratorCall(prog, param.Decorators()[0])
74
+ if ok == false || len(segments) < 2 || segments[len(segments)-2] != "WebSocketRoute" {
75
+ _ = call
76
+ return ""
77
+ }
78
+ return segments[len(segments)-1]
79
+ }
80
+
81
+ func nestiaCoreWebSocketParameterTypeName(param *shimast.Node) string {
82
+ if param == nil || param.AsParameterDeclaration() == nil || param.AsParameterDeclaration().Type == nil {
83
+ return ""
84
+ }
85
+ text := NodeText(param.AsParameterDeclaration().Type)
86
+ text = strings.TrimSpace(text)
87
+ if index := strings.Index(text, "<"); index >= 0 {
88
+ text = text[:index]
89
+ }
90
+ if index := strings.LastIndex(text, "."); index >= 0 {
91
+ text = text[index+1:]
92
+ }
93
+ return text
94
+ }
95
+
96
+ func nestiaCoreWebSocketDiagnostic(file *shimast.SourceFile, node *shimast.Node, kind string, message string) Diagnostic {
97
+ filePath := ""
98
+ line, column := 0, 0
99
+ if file != nil {
100
+ filePath = file.FileName()
101
+ if node != nil {
102
+ if pos := node.Pos(); pos >= 0 {
103
+ l, c := shimscanner.GetECMALineAndByteOffsetOfPosition(file, pos)
104
+ line, column = l+1, c+1
105
+ }
106
+ }
107
+ }
108
+ return Diagnostic{
109
+ File: filePath,
110
+ Line: line,
111
+ Column: column,
112
+ Code: "nestia.core." + kind,
113
+ Message: message,
114
+ }
115
+ }
@@ -0,0 +1,13 @@
1
+ package transform
2
+
3
+ // NewSourceRewrite builds a SourceRewrite from another Go module (e.g. the
4
+ // `@nestia/sdk` plugin). SourceRewrite keeps its span fields unexported so
5
+ // that the only way to produce one is this constructor or the in-package
6
+ // collectors — every rewrite therefore carries a validated [start,end) span.
7
+ func NewSourceRewrite(start, end int, replacement string) SourceRewrite {
8
+ return SourceRewrite{
9
+ start: start,
10
+ end: end,
11
+ replacement: replacement,
12
+ }
13
+ }
@@ -0,0 +1,285 @@
1
+ package transform
2
+
3
+ import (
4
+ "path/filepath"
5
+ "sort"
6
+ "strings"
7
+
8
+ shimast "github.com/microsoft/typescript-go/shim/ast"
9
+ shimcore "github.com/microsoft/typescript-go/shim/core"
10
+ "github.com/samchon/ttsc/packages/ttsc/driver"
11
+ )
12
+
13
+ type pathsRewriter struct {
14
+ basePath string
15
+ outDir string
16
+ patterns []pathsPattern
17
+ rootDir string
18
+ sourceFiles map[string]string
19
+ }
20
+
21
+ type pathsPattern struct {
22
+ pattern string
23
+ targets []string
24
+ }
25
+
26
+ func newPathsRewriter(prog *driver.Program) *pathsRewriter {
27
+ out := &pathsRewriter{sourceFiles: map[string]string{}}
28
+ if prog == nil || prog.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig == nil || prog.ParsedConfig.ParsedConfig.CompilerOptions == nil {
29
+ return out
30
+ }
31
+ options := prog.ParsedConfig.ParsedConfig.CompilerOptions
32
+ out.basePath = filepath.Clean(options.GetPathsBasePath(prog.Host.GetCurrentDirectory()))
33
+ out.outDir = optionalPath(options.OutDir, prog.Host.GetCurrentDirectory())
34
+ out.rootDir = optionalPath(options.RootDir, prog.Host.GetCurrentDirectory())
35
+ files := prog.SourceFiles()
36
+ if out.rootDir == "" {
37
+ out.rootDir = commonSourceDir(files)
38
+ }
39
+ for _, file := range files {
40
+ name := normalizePath(file.FileName())
41
+ out.sourceFiles[name] = name
42
+ out.sourceFiles[stripKnownSourceExtension(name)] = name
43
+ }
44
+ if options.Paths != nil {
45
+ for pattern, targets := range options.Paths.Entries() {
46
+ out.patterns = append(out.patterns, pathsPattern{
47
+ pattern: pattern,
48
+ targets: append([]string(nil), targets...),
49
+ })
50
+ }
51
+ }
52
+ sort.SliceStable(out.patterns, func(i, j int) bool {
53
+ return patternRank(out.patterns[i].pattern) > patternRank(out.patterns[j].pattern)
54
+ })
55
+ return out
56
+ }
57
+
58
+ func (r *pathsRewriter) applyAll(files []*shimast.SourceFile) {
59
+ if r == nil || len(r.patterns) == 0 {
60
+ return
61
+ }
62
+ for _, file := range files {
63
+ r.apply(file)
64
+ }
65
+ }
66
+
67
+ func (r *pathsRewriter) apply(file *shimast.SourceFile) {
68
+ if r == nil || file == nil || len(r.patterns) == 0 {
69
+ return
70
+ }
71
+ visitModuleSpecifiers(file.AsNode(), func(lit *shimast.Node) {
72
+ if lit == nil || lit.Kind != shimast.KindStringLiteral {
73
+ return
74
+ }
75
+ spec := lit.Text()
76
+ rewritten, ok := r.rewrite(file.FileName(), spec)
77
+ if ok && rewritten != spec {
78
+ lit.AsStringLiteral().Text = rewritten
79
+ lit.Flags |= shimast.NodeFlagsSynthesized
80
+ lit.Loc = shimcore.UndefinedTextRange()
81
+ }
82
+ })
83
+ }
84
+
85
+ func visitModuleSpecifiers(node *shimast.Node, visit func(*shimast.Node)) {
86
+ if node == nil {
87
+ return
88
+ }
89
+ switch node.Kind {
90
+ case shimast.KindImportDeclaration:
91
+ visit(node.AsImportDeclaration().ModuleSpecifier)
92
+ case shimast.KindExportDeclaration:
93
+ visit(node.AsExportDeclaration().ModuleSpecifier)
94
+ case shimast.KindImportEqualsDeclaration:
95
+ ref := node.AsImportEqualsDeclaration().ModuleReference
96
+ if ref != nil && ref.Kind == shimast.KindExternalModuleReference {
97
+ visit(ref.AsExternalModuleReference().Expression)
98
+ }
99
+ case shimast.KindImportType:
100
+ arg := node.AsImportTypeNode().Argument
101
+ if arg != nil && arg.Kind == shimast.KindLiteralType {
102
+ visit(arg.AsLiteralTypeNode().Literal)
103
+ }
104
+ case shimast.KindModuleDeclaration:
105
+ decl := node.AsModuleDeclaration()
106
+ if decl != nil {
107
+ visit(decl.Name())
108
+ }
109
+ case shimast.KindCallExpression:
110
+ call := node.AsCallExpression()
111
+ if isModuleSpecifierCall(call) && call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
112
+ visit(call.Arguments.Nodes[0])
113
+ }
114
+ }
115
+ node.ForEachChild(func(child *shimast.Node) bool {
116
+ visitModuleSpecifiers(child, visit)
117
+ return false
118
+ })
119
+ }
120
+
121
+ func isModuleSpecifierCall(call *shimast.CallExpression) bool {
122
+ if call == nil || call.Expression == nil {
123
+ return false
124
+ }
125
+ switch call.Expression.Kind {
126
+ case shimast.KindImportKeyword:
127
+ return true
128
+ case shimast.KindIdentifier:
129
+ return call.Expression.Text() == "require"
130
+ default:
131
+ return false
132
+ }
133
+ }
134
+
135
+ func (r *pathsRewriter) rewrite(fromSource string, specifier string) (string, bool) {
136
+ if specifier == "" || strings.HasPrefix(specifier, ".") || strings.HasPrefix(specifier, "/") {
137
+ return specifier, false
138
+ }
139
+ targetSource, ok := r.resolveSource(specifier)
140
+ if !ok {
141
+ return specifier, false
142
+ }
143
+ fromOut := r.outputPathForSource(fromSource)
144
+ targetOut := r.outputPathForSource(targetSource)
145
+ if fromOut == "" || targetOut == "" {
146
+ return specifier, false
147
+ }
148
+ rel, err := filepath.Rel(filepath.Dir(fromOut), targetOut)
149
+ if err != nil {
150
+ return specifier, false
151
+ }
152
+ rel = filepath.ToSlash(rel)
153
+ if !strings.HasPrefix(rel, ".") {
154
+ rel = "./" + rel
155
+ }
156
+ return rel, true
157
+ }
158
+
159
+ func (r *pathsRewriter) resolveSource(specifier string) (string, bool) {
160
+ for _, pattern := range r.patterns {
161
+ star, ok := matchPattern(pattern.pattern, specifier)
162
+ if !ok {
163
+ continue
164
+ }
165
+ for _, target := range pattern.targets {
166
+ candidate := strings.Replace(target, "*", star, 1)
167
+ resolved := normalizePath(filepath.Join(r.basePath, candidate))
168
+ if source, ok := r.lookupSource(resolved); ok {
169
+ return source, true
170
+ }
171
+ }
172
+ }
173
+ return "", false
174
+ }
175
+
176
+ func (r *pathsRewriter) lookupSource(candidate string) (string, bool) {
177
+ if source, ok := r.sourceFiles[normalizePath(candidate)]; ok {
178
+ return source, true
179
+ }
180
+ stem := stripKnownSourceExtension(normalizePath(candidate))
181
+ if source, ok := r.sourceFiles[stem]; ok {
182
+ return source, true
183
+ }
184
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
185
+ if source, ok := r.sourceFiles[stem+ext]; ok {
186
+ return source, true
187
+ }
188
+ }
189
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts"} {
190
+ if source, ok := r.sourceFiles[normalizePath(filepath.Join(stem, "index"+ext))]; ok {
191
+ return source, true
192
+ }
193
+ }
194
+ return "", false
195
+ }
196
+
197
+ func (r *pathsRewriter) outputPathForSource(source string) string {
198
+ if r.outDir == "" || r.rootDir == "" {
199
+ return ""
200
+ }
201
+ rel, err := filepath.Rel(r.rootDir, source)
202
+ if err != nil || isOutsideRelativePath(rel) {
203
+ return ""
204
+ }
205
+ return normalizePath(filepath.Join(r.outDir, replaceSourceExtension(rel, emittedJavaScriptExtension(rel))))
206
+ }
207
+
208
+ func emittedJavaScriptExtension(source string) string {
209
+ switch strings.ToLower(filepath.Ext(source)) {
210
+ case ".mts":
211
+ return ".mjs"
212
+ case ".cts":
213
+ return ".cjs"
214
+ default:
215
+ return ".js"
216
+ }
217
+ }
218
+
219
+ func matchPattern(pattern string, specifier string) (string, bool) {
220
+ if !strings.Contains(pattern, "*") {
221
+ return "", pattern == specifier
222
+ }
223
+ parts := strings.SplitN(pattern, "*", 2)
224
+ if !strings.HasPrefix(specifier, parts[0]) || !strings.HasSuffix(specifier, parts[1]) {
225
+ return "", false
226
+ }
227
+ return specifier[len(parts[0]) : len(specifier)-len(parts[1])], true
228
+ }
229
+
230
+ func patternRank(pattern string) int {
231
+ return len(strings.ReplaceAll(pattern, "*", ""))
232
+ }
233
+
234
+ func optionalPath(value string, cwd string) string {
235
+ if value == "" {
236
+ return ""
237
+ }
238
+ if filepath.IsAbs(value) {
239
+ return normalizePath(value)
240
+ }
241
+ return normalizePath(filepath.Join(cwd, value))
242
+ }
243
+
244
+ func commonSourceDir(files []*shimast.SourceFile) string {
245
+ if len(files) == 0 {
246
+ return ""
247
+ }
248
+ common := normalizePath(filepath.Dir(files[0].FileName()))
249
+ for _, file := range files[1:] {
250
+ dir := normalizePath(filepath.Dir(file.FileName()))
251
+ for common != "" && !strings.HasPrefix(dir+"/", common+"/") {
252
+ next := filepath.Dir(common)
253
+ if next == common {
254
+ return common
255
+ }
256
+ common = normalizePath(next)
257
+ }
258
+ }
259
+ return common
260
+ }
261
+
262
+ func normalizePath(value string) string {
263
+ if value == "" {
264
+ return ""
265
+ }
266
+ return filepath.ToSlash(filepath.Clean(value))
267
+ }
268
+
269
+ func isOutsideRelativePath(rel string) bool {
270
+ return rel == ".." || strings.HasPrefix(filepath.ToSlash(rel), "../")
271
+ }
272
+
273
+ func stripKnownSourceExtension(value string) string {
274
+ lower := strings.ToLower(value)
275
+ for _, ext := range []string{".d.ts", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
276
+ if strings.HasSuffix(lower, ext) {
277
+ return value[:len(value)-len(ext)]
278
+ }
279
+ }
280
+ return strings.TrimSuffix(value, filepath.Ext(value))
281
+ }
282
+
283
+ func replaceSourceExtension(value string, ext string) string {
284
+ return stripKnownSourceExtension(filepath.ToSlash(value)) + ext
285
+ }
@@ -0,0 +1,243 @@
1
+ package transform
2
+
3
+ import "testing"
4
+
5
+ // Verifies emittedJavaScriptExtension maps source-file extensions to the
6
+ // matching JavaScript output extension that tsgo writes.
7
+ //
8
+ // The native rewrite scan asks for the post-emit name of every registered
9
+ // source. A mismatch here means the rewrite searches for `.js` files when
10
+ // the project actually emits `.mjs` (ESM packages) or `.cjs`, and the
11
+ // "could not locate <call>" failure path then fires on legitimate
12
+ // builds. The case-insensitive switch covers Windows-style mixed-case
13
+ // `.MTS` paths some toolchains produce.
14
+ //
15
+ // 1. Cover the three explicit branches (.mts, .cts, default).
16
+ // 2. Cover the case-insensitive variants (.MTS, .CTS).
17
+ // 3. Cover the default fallback for unknown / extensionless inputs.
18
+ func TestEmittedJavaScriptExtensionMapsSourceToOutput(t *testing.T) {
19
+ cases := []struct {
20
+ name string
21
+ source string
22
+ want string
23
+ }{
24
+ {"plain ts", "/repo/src/index.ts", ".js"},
25
+ {"plain tsx", "/repo/src/Component.tsx", ".js"},
26
+ {"esm mts", "/repo/src/index.mts", ".mjs"},
27
+ {"cjs cts", "/repo/src/index.cts", ".cjs"},
28
+ {"uppercase mts", "/repo/src/Index.MTS", ".mjs"},
29
+ {"uppercase cts", "/repo/src/Index.CTS", ".cjs"},
30
+ {"declaration only", "/repo/src/types.d.ts", ".js"},
31
+ {"unknown extension", "/repo/src/data.json", ".js"},
32
+ {"extensionless", "/repo/src/README", ".js"},
33
+ }
34
+ for _, tc := range cases {
35
+ t.Run(tc.name, func(t *testing.T) {
36
+ if got := emittedJavaScriptExtension(tc.source); got != tc.want {
37
+ t.Fatalf("emittedJavaScriptExtension(%q) = %q, want %q", tc.source, got, tc.want)
38
+ }
39
+ })
40
+ }
41
+ }
42
+
43
+ // Verifies matchPattern strictly matches the literal-pattern branch and
44
+ // returns the wildcard capture for star-bearing patterns.
45
+ //
46
+ // `matchPattern` underpins every `paths` resolver entry — a regression in
47
+ // the prefix / suffix slice arithmetic would silently break tsconfig
48
+ // `paths` aliasing for any project that uses it. The literal branch must
49
+ // reject `pattern !== specifier`; the wildcard branch must demand BOTH
50
+ // the prefix and suffix to anchor before extracting the capture.
51
+ //
52
+ // 1. Literal patterns match only on byte equality.
53
+ // 2. Single-wildcard patterns extract the gap between prefix and suffix.
54
+ // 3. Mismatched prefix or suffix returns no capture.
55
+ func TestMatchPatternLiteralAndWildcardBranches(t *testing.T) {
56
+ cases := []struct {
57
+ name string
58
+ pattern string
59
+ input string
60
+ want string
61
+ matched bool
62
+ }{
63
+ {"literal match", "@api", "@api", "", true},
64
+ {"literal mismatch", "@api", "@api/lib", "", false},
65
+ {"wildcard captures middle", "@api/*", "@api/users", "users", true},
66
+ {"wildcard captures nested", "@api/lib/*", "@api/lib/sub/file", "sub/file", true},
67
+ {"wildcard mismatch prefix", "@api/*", "@other/users", "", false},
68
+ {"wildcard mismatch suffix", "@api/*.ts", "@api/users.tsx", "", false},
69
+ {"wildcard between prefix and suffix", "src/*/index.ts", "src/foo/index.ts", "foo", true},
70
+ {"empty capture allowed", "prefix*suffix", "prefixsuffix", "", true},
71
+ {"second wildcard treated as literal", "@api/*/lib/*", "@api/foo/lib/*", "foo", true},
72
+ {"second wildcard does not expand", "@api/*/lib/*", "@api/foo/lib/bar", "", false},
73
+ }
74
+ for _, tc := range cases {
75
+ t.Run(tc.name, func(t *testing.T) {
76
+ got, ok := matchPattern(tc.pattern, tc.input)
77
+ if ok != tc.matched {
78
+ t.Fatalf("matchPattern(%q, %q) ok = %v, want %v", tc.pattern, tc.input, ok, tc.matched)
79
+ }
80
+ if ok && got != tc.want {
81
+ t.Fatalf("matchPattern(%q, %q) capture = %q, want %q", tc.pattern, tc.input, got, tc.want)
82
+ }
83
+ })
84
+ }
85
+ }
86
+
87
+ // Verifies patternRank assigns longer literal-character counts higher rank
88
+ // so the longest non-wildcard pattern wins when several match.
89
+ //
90
+ // tsconfig `paths` resolution sorts candidates by `patternRank` descending
91
+ // to break ties — without this ordering the resolver could pick the
92
+ // loosest pattern over a more specific one (e.g. `*` over `@api/*`).
93
+ //
94
+ // 1. A pure-wildcard pattern ranks 0.
95
+ // 2. Each non-wildcard character adds 1 regardless of position.
96
+ // 3. Multiple wildcards do not double-count literal segments.
97
+ func TestPatternRankCountsNonWildcardCharacters(t *testing.T) {
98
+ cases := []struct {
99
+ pattern string
100
+ want int
101
+ }{
102
+ {"*", 0},
103
+ {"@api/*", 5},
104
+ {"@api/lib/*", 9},
105
+ {"src/*/index.ts", 13},
106
+ {"prefix*suffix", 12},
107
+ }
108
+ for _, tc := range cases {
109
+ if got := patternRank(tc.pattern); got != tc.want {
110
+ t.Fatalf("patternRank(%q) = %d, want %d", tc.pattern, got, tc.want)
111
+ }
112
+ }
113
+ }
114
+
115
+ // Verifies normalizePath collapses redundant POSIX separators and
116
+ // resolves `..` segments via filepath.Clean.
117
+ //
118
+ // The rewrite pipeline keys outputs and sources by their normalized
119
+ // path. A regression here would split a single logical source into
120
+ // multiple cache entries, defeating `nativeRewriteSet`'s deduplication.
121
+ // `filepath.ToSlash` runs after Clean — on Linux it is a no-op for
122
+ // already-POSIX input; backslash handling is delegated to the platform.
123
+ //
124
+ // 1. Empty input returns empty.
125
+ // 2. Adjacent separators collapse.
126
+ // 3. `..` segments are resolved relative to the preceding directory.
127
+ // 4. A leading `./` is stripped (filepath.Clean canonicalization).
128
+ func TestNormalizePathProducesPosixSlashes(t *testing.T) {
129
+ cases := []struct {
130
+ input string
131
+ want string
132
+ }{
133
+ {"", ""},
134
+ {"/repo/src/index.ts", "/repo/src/index.ts"},
135
+ {"/repo//src///index.ts", "/repo/src/index.ts"},
136
+ {"/repo/src/../src/index.ts", "/repo/src/index.ts"},
137
+ {"./relative/path", "relative/path"},
138
+ }
139
+ for _, tc := range cases {
140
+ if got := normalizePath(tc.input); got != tc.want {
141
+ t.Fatalf("normalizePath(%q) = %q, want %q", tc.input, got, tc.want)
142
+ }
143
+ }
144
+ }
145
+
146
+ // Verifies isOutsideRelativePath flags any relative path that escapes the
147
+ // root via `..` segments.
148
+ //
149
+ // `pathRewriter.outputForSource` refuses to map sources that resolve
150
+ // outside `rootDir`. Without this guard, a controller imported from a
151
+ // monorepo neighbor would emit into the outDir at a relative position
152
+ // that collides with an unrelated file.
153
+ //
154
+ // 1. Bare `..` is outside.
155
+ // 2. `../...` prefix is outside.
156
+ // 3. Same-or-nested paths are inside.
157
+ func TestIsOutsideRelativePathRejectsParentEscapes(t *testing.T) {
158
+ cases := []struct {
159
+ rel string
160
+ want bool
161
+ }{
162
+ {"..", true},
163
+ {"../sibling", true},
164
+ {"../../escape", true},
165
+ {".", false},
166
+ {"nested/file.ts", false},
167
+ {"sub/../same.ts", false},
168
+ }
169
+ for _, tc := range cases {
170
+ if got := isOutsideRelativePath(tc.rel); got != tc.want {
171
+ t.Fatalf("isOutsideRelativePath(%q) = %v, want %v", tc.rel, got, tc.want)
172
+ }
173
+ }
174
+ }
175
+
176
+ // Verifies stripKnownSourceExtension prefers the longest known suffix and
177
+ // falls back to filepath.Ext for unknown extensions.
178
+ //
179
+ // The declaration-file extensions (`.d.ts`, `.d.mts`, `.d.cts`) must be
180
+ // matched as a whole — falling through to the simpler `.ts` strip would
181
+ // leave the `.d` suffix on the stem and silently produce wrong virtual
182
+ // paths for declaration emitters.
183
+ //
184
+ // 1. `.d.ts` strips the full three-character suffix.
185
+ // 2. Each one-extension form (`.ts`, `.tsx`, `.mts`, …) strips cleanly.
186
+ // 3. Unknown extensions fall back to filepath.Ext.
187
+ // 4. Case-insensitive match on uppercase variants.
188
+ func TestStripKnownSourceExtensionPrefersLongestMatch(t *testing.T) {
189
+ cases := []struct {
190
+ input string
191
+ want string
192
+ }{
193
+ {"types.d.ts", "types"},
194
+ {"types.d.mts", "types"},
195
+ {"types.d.cts", "types"},
196
+ {"index.ts", "index"},
197
+ {"index.tsx", "index"},
198
+ {"index.mts", "index"},
199
+ {"index.cts", "index"},
200
+ {"index.js", "index"},
201
+ {"data.json", "data"},
202
+ {"Index.D.TS", "Index"},
203
+ {"README", "README"},
204
+ }
205
+ for _, tc := range cases {
206
+ if got := stripKnownSourceExtension(tc.input); got != tc.want {
207
+ t.Fatalf("stripKnownSourceExtension(%q) = %q, want %q", tc.input, got, tc.want)
208
+ }
209
+ }
210
+ }
211
+
212
+ // Verifies replaceSourceExtension swaps the known TypeScript extension
213
+ // while preserving the rest of the path.
214
+ //
215
+ // Combined with `emittedJavaScriptExtension`, this drives the source ↔
216
+ // output filename mapping the rewrite scan depends on. The declaration
217
+ // case is load-bearing: `stripKnownSourceExtension` must prefer the full
218
+ // `.d.ts` suffix over a plain `.ts` strip, otherwise `types.d.ts +
219
+ // .d.ts` would silently become `types..d.ts` and the declaration
220
+ // emitter would write a corrupt path.
221
+ //
222
+ // 1. `.ts` is replaced with the chosen output extension.
223
+ // 2. Declaration suffixes (`.d.ts`) strip wholly before replacement.
224
+ // 3. POSIX separators in the input flow through unchanged.
225
+ // 4. `.d.ts` + `.d.ts` stays `.d.ts` (pins the longest-match prefer rule).
226
+ func TestReplaceSourceExtensionSwapsKnownSuffix(t *testing.T) {
227
+ cases := []struct {
228
+ input string
229
+ ext string
230
+ want string
231
+ }{
232
+ {"src/index.ts", ".js", "src/index.js"},
233
+ {"src/index.mts", ".mjs", "src/index.mjs"},
234
+ {"src/types.d.ts", ".js", "src/types.js"},
235
+ {"src/nested/foo.ts", ".js", "src/nested/foo.js"},
236
+ {"src/types.d.ts", ".d.ts", "src/types.d.ts"},
237
+ }
238
+ for _, tc := range cases {
239
+ if got := replaceSourceExtension(tc.input, tc.ext); got != tc.want {
240
+ t.Fatalf("replaceSourceExtension(%q, %q) = %q, want %q", tc.input, tc.ext, got, tc.want)
241
+ }
242
+ }
243
+ }