@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.
- package/MIGRATION.md +169 -0
- package/lib/adaptors/WebSocketAdaptor.js +7 -3
- package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
- package/lib/decorators/DynamicModule.js.map +1 -1
- package/lib/decorators/EncryptedBody.js.map +1 -1
- package/lib/decorators/EncryptedController.js.map +1 -1
- package/lib/decorators/EncryptedModule.js.map +1 -1
- package/lib/decorators/EncryptedRoute.js.map +1 -1
- package/lib/decorators/HumanRoute.js.map +1 -1
- package/lib/decorators/NoTransformConfigurationError.js +5 -2
- package/lib/decorators/NoTransformConfigurationError.js.map +1 -1
- package/lib/decorators/PlainBody.js.map +1 -1
- package/lib/decorators/SwaggerCustomizer.js.map +1 -1
- package/lib/decorators/SwaggerExample.js.map +1 -1
- package/lib/decorators/TypedBody.js.map +1 -1
- package/lib/decorators/TypedException.js.map +1 -1
- package/lib/decorators/TypedFormData.js.map +1 -1
- package/lib/decorators/TypedHeaders.js.map +1 -1
- package/lib/decorators/TypedParam.js +5 -2
- package/lib/decorators/TypedParam.js.map +1 -1
- package/lib/decorators/TypedQuery.js.map +1 -1
- package/lib/decorators/TypedRoute.js.map +1 -1
- package/lib/decorators/WebSocketRoute.js.map +1 -1
- package/lib/decorators/doNotThrowTransformError.js.map +1 -1
- package/lib/decorators/internal/get_path_and_querify.js +5 -2
- package/lib/decorators/internal/get_path_and_querify.js.map +1 -1
- package/lib/decorators/internal/get_path_and_stringify.js +5 -2
- package/lib/decorators/internal/get_path_and_stringify.js.map +1 -1
- package/lib/decorators/internal/get_text_body.js.map +1 -1
- package/lib/decorators/internal/headers_to_object.js.map +1 -1
- package/lib/decorators/internal/is_request_body_undefined.js.map +1 -1
- package/lib/decorators/internal/load_controller.js +48 -16
- package/lib/decorators/internal/load_controller.js.map +1 -1
- package/lib/decorators/internal/route_error.js +3 -3
- package/lib/decorators/internal/route_error.js.map +1 -1
- package/lib/decorators/internal/validate_request_body.js +5 -2
- package/lib/decorators/internal/validate_request_body.js.map +1 -1
- package/lib/decorators/internal/validate_request_form_data.js +5 -2
- package/lib/decorators/internal/validate_request_form_data.js.map +1 -1
- package/lib/decorators/internal/validate_request_headers.js +5 -2
- package/lib/decorators/internal/validate_request_headers.js.map +1 -1
- package/lib/decorators/internal/validate_request_query.js +30 -4
- package/lib/decorators/internal/validate_request_query.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/transform.d.ts +10 -5
- package/lib/transform.js +64 -28
- package/lib/transform.js.map +1 -1
- package/lib/utils/ArrayUtil.js.map +1 -1
- package/lib/utils/ExceptionManager.js.map +1 -1
- package/lib/utils/Singleton.js.map +1 -1
- package/lib/utils/SourceFinder.js.map +1 -1
- package/lib/utils/VersioningStrategy.js.map +1 -1
- package/native/cmd/ttsc-nestia/main.go +11 -0
- package/native/go.mod +32 -0
- package/native/go.sum +54 -0
- package/native/plugin/plan.go +102 -0
- package/native/transform/ast.go +32 -0
- package/native/transform/build.go +413 -0
- package/native/transform/cleanup.go +408 -0
- 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/transform/core_querify.go +227 -0
- package/native/transform/core_transform.go +1713 -0
- package/native/transform/core_websocket.go +115 -0
- package/native/transform/exports.go +13 -0
- package/native/transform/path_rewrite.go +285 -0
- package/native/transform/path_rewrite_test.go +243 -0
- package/native/transform/printer.go +244 -0
- package/native/transform/rewrite.go +662 -0
- 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/transform/transform.go +376 -0
- package/native/transform/typia_fast.go +326 -0
- package/native/transform/typia_replacement.go +24 -0
- package/native/transform.cjs +43 -0
- package/package.json +28 -22
- package/src/decorators/NoTransformConfigurationError.ts +5 -2
- package/src/decorators/internal/load_controller.ts +50 -19
- package/src/decorators/internal/validate_request_query.ts +21 -2
- package/src/transform.ts +101 -35
- package/lib/decorators/internal/NoTransformConfigureError.d.ts +0 -1
- package/lib/decorators/internal/NoTransformConfigureError.js +0 -7
- package/lib/decorators/internal/NoTransformConfigureError.js.map +0 -1
- package/lib/options/INestiaTransformOptions.d.ts +0 -13
- package/lib/options/INestiaTransformOptions.js +0 -3
- package/lib/options/INestiaTransformOptions.js.map +0 -1
- package/lib/options/INestiaTransformProject.d.ts +0 -5
- package/lib/options/INestiaTransformProject.js +0 -3
- package/lib/options/INestiaTransformProject.js.map +0 -1
- package/lib/programmers/PlainBodyProgrammer.d.ts +0 -9
- package/lib/programmers/PlainBodyProgrammer.js +0 -58
- package/lib/programmers/PlainBodyProgrammer.js.map +0 -1
- package/lib/programmers/TypedBodyProgrammer.d.ts +0 -9
- package/lib/programmers/TypedBodyProgrammer.js +0 -94
- package/lib/programmers/TypedBodyProgrammer.js.map +0 -1
- package/lib/programmers/TypedFormDataBodyProgrammer.d.ts +0 -9
- package/lib/programmers/TypedFormDataBodyProgrammer.js +0 -65
- package/lib/programmers/TypedFormDataBodyProgrammer.js.map +0 -1
- package/lib/programmers/TypedHeadersProgrammer.d.ts +0 -9
- package/lib/programmers/TypedHeadersProgrammer.js +0 -38
- package/lib/programmers/TypedHeadersProgrammer.js.map +0 -1
- package/lib/programmers/TypedParamProgrammer.d.ts +0 -10
- package/lib/programmers/TypedParamProgrammer.js +0 -32
- package/lib/programmers/TypedParamProgrammer.js.map +0 -1
- package/lib/programmers/TypedQueryBodyProgrammer.d.ts +0 -9
- package/lib/programmers/TypedQueryBodyProgrammer.js +0 -74
- package/lib/programmers/TypedQueryBodyProgrammer.js.map +0 -1
- package/lib/programmers/TypedQueryProgrammer.d.ts +0 -9
- package/lib/programmers/TypedQueryProgrammer.js +0 -75
- package/lib/programmers/TypedQueryProgrammer.js.map +0 -1
- package/lib/programmers/TypedQueryRouteProgrammer.d.ts +0 -9
- package/lib/programmers/TypedQueryRouteProgrammer.js +0 -75
- package/lib/programmers/TypedQueryRouteProgrammer.js.map +0 -1
- package/lib/programmers/TypedRouteProgrammer.d.ts +0 -9
- package/lib/programmers/TypedRouteProgrammer.js +0 -79
- package/lib/programmers/TypedRouteProgrammer.js.map +0 -1
- package/lib/programmers/http/HttpAssertQuerifyProgrammer.d.ts +0 -9
- package/lib/programmers/http/HttpAssertQuerifyProgrammer.js +0 -39
- package/lib/programmers/http/HttpAssertQuerifyProgrammer.js.map +0 -1
- package/lib/programmers/http/HttpIsQuerifyProgrammer.d.ts +0 -9
- package/lib/programmers/http/HttpIsQuerifyProgrammer.js +0 -36
- package/lib/programmers/http/HttpIsQuerifyProgrammer.js.map +0 -1
- package/lib/programmers/http/HttpQuerifyProgrammer.d.ts +0 -9
- package/lib/programmers/http/HttpQuerifyProgrammer.js +0 -50
- package/lib/programmers/http/HttpQuerifyProgrammer.js.map +0 -1
- package/lib/programmers/http/HttpValidateQuerifyProgrammer.d.ts +0 -9
- package/lib/programmers/http/HttpValidateQuerifyProgrammer.js +0 -40
- package/lib/programmers/http/HttpValidateQuerifyProgrammer.js.map +0 -1
- package/lib/programmers/internal/CoreMetadataUtil.d.ts +0 -5
- package/lib/programmers/internal/CoreMetadataUtil.js +0 -19
- package/lib/programmers/internal/CoreMetadataUtil.js.map +0 -1
- package/lib/transformers/FileTransformer.d.ts +0 -5
- package/lib/transformers/FileTransformer.js +0 -80
- package/lib/transformers/FileTransformer.js.map +0 -1
- package/lib/transformers/MethodTransformer.d.ts +0 -8
- package/lib/transformers/MethodTransformer.js +0 -58
- package/lib/transformers/MethodTransformer.js.map +0 -1
- package/lib/transformers/NodeTransformer.d.ts +0 -8
- package/lib/transformers/NodeTransformer.js +0 -24
- package/lib/transformers/NodeTransformer.js.map +0 -1
- package/lib/transformers/ParameterDecoratorTransformer.d.ts +0 -9
- package/lib/transformers/ParameterDecoratorTransformer.js +0 -104
- package/lib/transformers/ParameterDecoratorTransformer.js.map +0 -1
- package/lib/transformers/ParameterTransformer.d.ts +0 -8
- package/lib/transformers/ParameterTransformer.js +0 -37
- package/lib/transformers/ParameterTransformer.js.map +0 -1
- package/lib/transformers/TypedRouteTransformer.d.ts +0 -9
- package/lib/transformers/TypedRouteTransformer.js +0 -68
- package/lib/transformers/TypedRouteTransformer.js.map +0 -1
- package/lib/transformers/WebSocketRouteTransformer.d.ts +0 -9
- package/lib/transformers/WebSocketRouteTransformer.js +0 -72
- package/lib/transformers/WebSocketRouteTransformer.js.map +0 -1
- package/src/decorators/internal/NoTransformConfigureError.ts +0 -2
- package/src/options/INestiaTransformOptions.ts +0 -34
- package/src/options/INestiaTransformProject.ts +0 -10
- package/src/programmers/PlainBodyProgrammer.ts +0 -72
- package/src/programmers/TypedBodyProgrammer.ts +0 -148
- package/src/programmers/TypedFormDataBodyProgrammer.ts +0 -118
- package/src/programmers/TypedHeadersProgrammer.ts +0 -65
- package/src/programmers/TypedParamProgrammer.ts +0 -33
- package/src/programmers/TypedQueryBodyProgrammer.ts +0 -113
- package/src/programmers/TypedQueryProgrammer.ts +0 -115
- package/src/programmers/TypedQueryRouteProgrammer.ts +0 -107
- package/src/programmers/TypedRouteProgrammer.ts +0 -103
- package/src/programmers/http/HttpAssertQuerifyProgrammer.ts +0 -74
- package/src/programmers/http/HttpIsQuerifyProgrammer.ts +0 -77
- package/src/programmers/http/HttpQuerifyProgrammer.ts +0 -110
- package/src/programmers/http/HttpValidateQuerifyProgrammer.ts +0 -78
- package/src/programmers/internal/CoreMetadataUtil.ts +0 -21
- package/src/transformers/FileTransformer.ts +0 -109
- package/src/transformers/MethodTransformer.ts +0 -103
- package/src/transformers/NodeTransformer.ts +0 -23
- package/src/transformers/ParameterDecoratorTransformer.ts +0 -143
- package/src/transformers/ParameterTransformer.ts +0 -57
- package/src/transformers/TypedRouteTransformer.ts +0 -85
- 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
|
+
}
|