@nestia/core 12.0.0-dev.20260520.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.
- package/lib/transform.d.ts +8 -0
- package/lib/transform.js +28 -12
- package/lib/transform.js.map +1 -1
- package/native/cmd/ttsc-nestia/main.go +2 -63
- package/native/go.mod +1 -1
- package/native/transform/ast.go +32 -0
- package/native/{cmd/ttsc-nestia → transform}/build.go +14 -35
- package/native/{cmd/ttsc-nestia → transform}/cleanup.go +1 -1
- package/native/transform/cleanup_test.go +76 -0
- package/native/transform/commonjs_import_alias_test.go +49 -0
- package/native/transform/core_dispatch_test.go +127 -0
- package/native/{cmd/ttsc-nestia → transform}/core_querify.go +1 -1
- package/native/{cmd/ttsc-nestia → transform}/core_transform.go +26 -26
- package/native/{cmd/ttsc-nestia → transform}/core_websocket.go +10 -10
- package/native/transform/exports.go +13 -0
- package/native/{cmd/ttsc-nestia → transform}/path_rewrite.go +1 -1
- package/native/transform/path_rewrite_test.go +243 -0
- package/native/{cmd/ttsc-nestia → transform}/printer.go +1 -1
- package/native/{cmd/ttsc-nestia → transform}/rewrite.go +3 -3
- package/native/transform/rewrite_test.go +118 -0
- package/native/transform/rewrite_unique_base_test.go +48 -0
- package/native/transform/run.go +72 -0
- package/native/{cmd/ttsc-nestia → transform}/transform.go +25 -36
- package/native/{cmd/ttsc-nestia → transform}/typia_fast.go +1 -1
- package/native/{cmd/ttsc-nestia → transform}/typia_replacement.go +1 -1
- package/native/transform.cjs +34 -12
- package/package.json +8 -7
- package/src/transform.ts +39 -20
- package/native/cmd/ttsc-nestia/sdk_metadata_json.go +0 -327
- package/native/cmd/ttsc-nestia/sdk_transform.go +0 -1541
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package transform
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
@@ -79,7 +79,7 @@ func newNestiaCoreTransformState(prog *driver.Program, options nestiaCoreOptions
|
|
|
79
79
|
|
|
80
80
|
var nestiaCoreFactory = shimast.NewNodeFactory(shimast.NodeFactoryHooks{})
|
|
81
81
|
|
|
82
|
-
const
|
|
82
|
+
const NestiaCoreKindDecorator = shimast.KindDecorator
|
|
83
83
|
|
|
84
84
|
type nestiaCoreFileContext struct {
|
|
85
85
|
file *shimast.SourceFile
|
|
@@ -121,18 +121,18 @@ func collectNestiaCoreSourceRewriteMap(
|
|
|
121
121
|
prog *driver.Program,
|
|
122
122
|
plan plugin.Plan,
|
|
123
123
|
onlyFile string,
|
|
124
|
-
) (map[string][]
|
|
124
|
+
) (map[string][]SourceRewrite, []Diagnostic) {
|
|
125
125
|
if plan.Core == false {
|
|
126
|
-
return map[string][]
|
|
126
|
+
return map[string][]SourceRewrite{}, nil
|
|
127
127
|
}
|
|
128
128
|
options := readNestiaCoreOptions(plan)
|
|
129
129
|
sites, diagnostics := collectNestiaCoreSites(newNestiaCoreTransformState(prog, options))
|
|
130
|
-
rewrites := map[string][]
|
|
130
|
+
rewrites := map[string][]SourceRewrite{}
|
|
131
131
|
for _, site := range sites {
|
|
132
132
|
if onlyFile != "" && filepath.ToSlash(site.FilePath) != filepath.ToSlash(onlyFile) {
|
|
133
133
|
continue
|
|
134
134
|
}
|
|
135
|
-
source, ok :=
|
|
135
|
+
source, ok := SourceFileText(site.File)
|
|
136
136
|
if !ok {
|
|
137
137
|
diagnostics = append(diagnostics, nestiaCoreDiagnostic(site, "source text is unavailable"))
|
|
138
138
|
continue
|
|
@@ -143,7 +143,7 @@ func collectNestiaCoreSourceRewriteMap(
|
|
|
143
143
|
continue
|
|
144
144
|
}
|
|
145
145
|
replacement := appendArgumentsText(source[open+1:close], site.Arguments)
|
|
146
|
-
rewrites[filepath.ToSlash(site.FilePath)] = append(rewrites[filepath.ToSlash(site.FilePath)],
|
|
146
|
+
rewrites[filepath.ToSlash(site.FilePath)] = append(rewrites[filepath.ToSlash(site.FilePath)], SourceRewrite{
|
|
147
147
|
start: open + 1,
|
|
148
148
|
end: close,
|
|
149
149
|
replacement: replacement,
|
|
@@ -156,7 +156,7 @@ func collectNestiaCoreBuildRewrites(
|
|
|
156
156
|
prog *driver.Program,
|
|
157
157
|
plan plugin.Plan,
|
|
158
158
|
rewrites *nativeRewriteSet,
|
|
159
|
-
) []
|
|
159
|
+
) []Diagnostic {
|
|
160
160
|
if plan.Core == false {
|
|
161
161
|
return nil
|
|
162
162
|
}
|
|
@@ -180,7 +180,7 @@ func collectNestiaCoreBuildRewrites(
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
func nestiaCoreOriginalArgumentText(site nestiaCoreSite) string {
|
|
183
|
-
source, ok :=
|
|
183
|
+
source, ok := SourceFileText(site.File)
|
|
184
184
|
if !ok {
|
|
185
185
|
return ""
|
|
186
186
|
}
|
|
@@ -199,9 +199,9 @@ func nestiaCoreStableOriginalArgumentText(site nestiaCoreSite) string {
|
|
|
199
199
|
return text
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
func collectNestiaCoreSites(state *nestiaCoreTransformState) ([]nestiaCoreSite, []
|
|
202
|
+
func collectNestiaCoreSites(state *nestiaCoreTransformState) ([]nestiaCoreSite, []Diagnostic) {
|
|
203
203
|
sites := []nestiaCoreSite{}
|
|
204
|
-
diagnostics := []
|
|
204
|
+
diagnostics := []Diagnostic{}
|
|
205
205
|
prog := state.prog
|
|
206
206
|
if nestiaCoreStrictMode(prog) == false {
|
|
207
207
|
diagnostics = append(diagnostics, nestiaCoreGlobalDiagnostic("@nestia/core", "strict mode is required."))
|
|
@@ -227,7 +227,7 @@ func visitNestiaCoreNode(
|
|
|
227
227
|
context nestiaCoreFileContext,
|
|
228
228
|
node *shimast.Node,
|
|
229
229
|
sites *[]nestiaCoreSite,
|
|
230
|
-
diagnostics *[]
|
|
230
|
+
diagnostics *[]Diagnostic,
|
|
231
231
|
) {
|
|
232
232
|
if node == nil {
|
|
233
233
|
return
|
|
@@ -317,7 +317,7 @@ func visitNestiaCoreNode(
|
|
|
317
317
|
if len(candidates) == 0 {
|
|
318
318
|
break
|
|
319
319
|
}
|
|
320
|
-
typ :=
|
|
320
|
+
typ := NestiaCoreMethodReturnType(state.prog, node)
|
|
321
321
|
if typ != nil {
|
|
322
322
|
for _, candidate := range candidates {
|
|
323
323
|
site, ok, err := transformNestiaCoreMethodDecorator(
|
|
@@ -489,7 +489,7 @@ func (state *nestiaCoreTransformState) cacheKey(
|
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
func nestiaCoreRawDecoratorCall(decorator *shimast.Node) (*shimast.CallExpression, []string, bool) {
|
|
492
|
-
if decorator == nil || decorator.Kind !=
|
|
492
|
+
if decorator == nil || decorator.Kind != NestiaCoreKindDecorator {
|
|
493
493
|
return nil, nil, false
|
|
494
494
|
}
|
|
495
495
|
expression := decorator.AsDecorator().Expression
|
|
@@ -497,7 +497,7 @@ func nestiaCoreRawDecoratorCall(decorator *shimast.Node) (*shimast.CallExpressio
|
|
|
497
497
|
return nil, nil, false
|
|
498
498
|
}
|
|
499
499
|
call := expression.AsCallExpression()
|
|
500
|
-
segments :=
|
|
500
|
+
segments := NestiaCoreExpressionSegments(call.Expression)
|
|
501
501
|
if len(segments) == 0 {
|
|
502
502
|
return nil, nil, false
|
|
503
503
|
}
|
|
@@ -600,7 +600,7 @@ func nestiaCoreDecoratorReference(
|
|
|
600
600
|
if nestiaCorePotentialDecoratorSegments(canonical) == false {
|
|
601
601
|
return false
|
|
602
602
|
}
|
|
603
|
-
return
|
|
603
|
+
return IsNestiaCoreCall(prog, decorator.AsDecorator().Expression)
|
|
604
604
|
}
|
|
605
605
|
|
|
606
606
|
func nestiaCorePotentialDecoratorSegments(segments []string) bool {
|
|
@@ -609,7 +609,7 @@ func nestiaCorePotentialDecoratorSegments(segments []string) bool {
|
|
|
609
609
|
(len(segments) != 0 && segments[len(segments)-1] == "WebSocketRoute")
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
func
|
|
612
|
+
func IsNestiaCoreCall(prog *driver.Program, node *shimast.Node) bool {
|
|
613
613
|
signature := prog.Checker.GetResolvedSignature(node)
|
|
614
614
|
if signature == nil || signature.Declaration() == nil {
|
|
615
615
|
return false
|
|
@@ -1406,7 +1406,7 @@ func cleanupNestiaCorePrintedExpression(text string) string {
|
|
|
1406
1406
|
|
|
1407
1407
|
var nestiaCoreSingleParameterArrowPattern = regexp.MustCompile(`(^|[\s(=,:?])([A-Za-z_$][A-Za-z0-9_$]*) =>`)
|
|
1408
1408
|
|
|
1409
|
-
func
|
|
1409
|
+
func NestiaCoreMethodReturnType(prog *driver.Program, node *shimast.Node) *shimchecker.Type {
|
|
1410
1410
|
signature := prog.Checker.GetSignatureFromDeclaration(node)
|
|
1411
1411
|
if signature == nil {
|
|
1412
1412
|
return nil
|
|
@@ -1449,7 +1449,7 @@ func nestiaCoreShouldSkipMethodDecorator(prog *driver.Program, call *shimast.Cal
|
|
|
1449
1449
|
}
|
|
1450
1450
|
|
|
1451
1451
|
func nestiaCoreHasPathLiteralArgument(call *shimast.CallExpression) bool {
|
|
1452
|
-
source, ok :=
|
|
1452
|
+
source, ok := SourceFileText(shimast.GetSourceFileOfNode(call.AsNode()))
|
|
1453
1453
|
if !ok {
|
|
1454
1454
|
return false
|
|
1455
1455
|
}
|
|
@@ -1498,7 +1498,7 @@ func appendArgumentsText(current string, arguments []string) string {
|
|
|
1498
1498
|
return current + ", " + next
|
|
1499
1499
|
}
|
|
1500
1500
|
|
|
1501
|
-
func
|
|
1501
|
+
func NestiaCoreExpressionSegments(node *shimast.Node) []string {
|
|
1502
1502
|
if node == nil {
|
|
1503
1503
|
return nil
|
|
1504
1504
|
}
|
|
@@ -1512,7 +1512,7 @@ func nestiaCoreExpressionSegments(node *shimast.Node) []string {
|
|
|
1512
1512
|
if access == nil {
|
|
1513
1513
|
return nil
|
|
1514
1514
|
}
|
|
1515
|
-
left :=
|
|
1515
|
+
left := NestiaCoreExpressionSegments(access.Expression)
|
|
1516
1516
|
name := access.Name()
|
|
1517
1517
|
if len(left) == 0 || name == nil || name.Kind != shimast.KindIdentifier {
|
|
1518
1518
|
return nil
|
|
@@ -1523,7 +1523,7 @@ func nestiaCoreExpressionSegments(node *shimast.Node) []string {
|
|
|
1523
1523
|
}
|
|
1524
1524
|
|
|
1525
1525
|
func nestiaCoreModuloNode(node *shimast.Node) *shimast.Node {
|
|
1526
|
-
segments :=
|
|
1526
|
+
segments := NestiaCoreExpressionSegments(node)
|
|
1527
1527
|
if len(segments) == 0 {
|
|
1528
1528
|
return nestiaCoreFactory.NewIdentifier("nestia_core_transform")
|
|
1529
1529
|
}
|
|
@@ -1688,7 +1688,7 @@ func commonJSImportAliasBase(module string) string {
|
|
|
1688
1688
|
return text
|
|
1689
1689
|
}
|
|
1690
1690
|
|
|
1691
|
-
func nestiaCoreDiagnostic(site nestiaCoreSite, message string)
|
|
1691
|
+
func nestiaCoreDiagnostic(site nestiaCoreSite, message string) Diagnostic {
|
|
1692
1692
|
line, column := 0, 0
|
|
1693
1693
|
if site.File != nil && site.Call != nil {
|
|
1694
1694
|
if pos := site.Call.AsNode().Pos(); pos >= 0 {
|
|
@@ -1696,7 +1696,7 @@ func nestiaCoreDiagnostic(site nestiaCoreSite, message string) typiaTransformDia
|
|
|
1696
1696
|
line, column = l+1, c+1
|
|
1697
1697
|
}
|
|
1698
1698
|
}
|
|
1699
|
-
return
|
|
1699
|
+
return Diagnostic{
|
|
1700
1700
|
File: site.FilePath,
|
|
1701
1701
|
Line: line,
|
|
1702
1702
|
Column: column,
|
|
@@ -1705,8 +1705,8 @@ func nestiaCoreDiagnostic(site nestiaCoreSite, message string) typiaTransformDia
|
|
|
1705
1705
|
}
|
|
1706
1706
|
}
|
|
1707
1707
|
|
|
1708
|
-
func nestiaCoreGlobalDiagnostic(code string, message string)
|
|
1709
|
-
return
|
|
1708
|
+
func nestiaCoreGlobalDiagnostic(code string, message string) Diagnostic {
|
|
1709
|
+
return Diagnostic{
|
|
1710
1710
|
Code: code,
|
|
1711
1711
|
Message: message,
|
|
1712
1712
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package transform
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
@@ -15,23 +15,23 @@ func validateNestiaCoreWebSocketRoute(
|
|
|
15
15
|
method *shimast.Node,
|
|
16
16
|
call *shimast.CallExpression,
|
|
17
17
|
segments []string,
|
|
18
|
-
) []
|
|
18
|
+
) []Diagnostic {
|
|
19
19
|
if call == nil || len(segments) == 0 || segments[len(segments)-1] != "WebSocketRoute" {
|
|
20
20
|
_ = call
|
|
21
21
|
return nil
|
|
22
22
|
}
|
|
23
|
-
diagnostics := []
|
|
23
|
+
diagnostics := []Diagnostic{}
|
|
24
24
|
accepted := false
|
|
25
25
|
methodDecl := method.AsMethodDeclaration()
|
|
26
26
|
if methodDecl == nil || methodDecl.Parameters == nil {
|
|
27
|
-
return []
|
|
27
|
+
return []Diagnostic{nestiaCoreWebSocketDiagnostic(context.file, method, "WebSocketRoute", fmt.Sprintf(
|
|
28
28
|
"method %q must have at least one parameter decorated by @WebSocketRoute.Acceptor().",
|
|
29
|
-
|
|
29
|
+
NodeName(method),
|
|
30
30
|
))}
|
|
31
31
|
}
|
|
32
32
|
for _, param := range methodDecl.Parameters.Nodes {
|
|
33
33
|
category := nestiaCoreWebSocketParameterCategory(prog, param)
|
|
34
|
-
name :=
|
|
34
|
+
name := NodeName(param)
|
|
35
35
|
if category == "" {
|
|
36
36
|
diagnostics = append(diagnostics, nestiaCoreWebSocketDiagnostic(context.file, param, "WebSocketRoute", fmt.Sprintf(
|
|
37
37
|
"parameter %q is not decorated with nested function of WebSocketRoute module.",
|
|
@@ -60,7 +60,7 @@ func validateNestiaCoreWebSocketRoute(
|
|
|
60
60
|
if accepted == false {
|
|
61
61
|
diagnostics = append(diagnostics, nestiaCoreWebSocketDiagnostic(context.file, method, "WebSocketRoute", fmt.Sprintf(
|
|
62
62
|
"method %q must have at least one parameter decorated by @WebSocketRoute.Acceptor().",
|
|
63
|
-
|
|
63
|
+
NodeName(method),
|
|
64
64
|
)))
|
|
65
65
|
}
|
|
66
66
|
return diagnostics
|
|
@@ -82,7 +82,7 @@ func nestiaCoreWebSocketParameterTypeName(param *shimast.Node) string {
|
|
|
82
82
|
if param == nil || param.AsParameterDeclaration() == nil || param.AsParameterDeclaration().Type == nil {
|
|
83
83
|
return ""
|
|
84
84
|
}
|
|
85
|
-
text :=
|
|
85
|
+
text := NodeText(param.AsParameterDeclaration().Type)
|
|
86
86
|
text = strings.TrimSpace(text)
|
|
87
87
|
if index := strings.Index(text, "<"); index >= 0 {
|
|
88
88
|
text = text[:index]
|
|
@@ -93,7 +93,7 @@ func nestiaCoreWebSocketParameterTypeName(param *shimast.Node) string {
|
|
|
93
93
|
return text
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
func nestiaCoreWebSocketDiagnostic(file *shimast.SourceFile, node *shimast.Node, kind string, message string)
|
|
96
|
+
func nestiaCoreWebSocketDiagnostic(file *shimast.SourceFile, node *shimast.Node, kind string, message string) Diagnostic {
|
|
97
97
|
filePath := ""
|
|
98
98
|
line, column := 0, 0
|
|
99
99
|
if file != nil {
|
|
@@ -105,7 +105,7 @@ func nestiaCoreWebSocketDiagnostic(file *shimast.SourceFile, node *shimast.Node,
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
-
return
|
|
108
|
+
return Diagnostic{
|
|
109
109
|
File: filePath,
|
|
110
110
|
Line: line,
|
|
111
111
|
Column: column,
|
|
@@ -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,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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package transform
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"fmt"
|
|
@@ -163,7 +163,7 @@ func (rs *nativeRewriteSet) findSourceForOutput(outputName string) (string, bool
|
|
|
163
163
|
outSlash := strings.TrimSuffix(filepath.ToSlash(outputName), filepath.Ext(outputName))
|
|
164
164
|
for path := range rs.byPath {
|
|
165
165
|
srcStem := strings.TrimSuffix(filepath.ToSlash(path), filepath.Ext(path))
|
|
166
|
-
if
|
|
166
|
+
if OutputMatchesSourceStem(outSlash, srcStem) {
|
|
167
167
|
return path, true
|
|
168
168
|
}
|
|
169
169
|
}
|
|
@@ -208,7 +208,7 @@ func outputHasBuildMarker(stem string) bool {
|
|
|
208
208
|
return false
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
func
|
|
211
|
+
func OutputMatchesSourceStem(outputStem string, sourceStem string) bool {
|
|
212
212
|
if outputStem == sourceStem {
|
|
213
213
|
return true
|
|
214
214
|
}
|
|
@@ -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
|
+
}
|