@zyno-io/ts-reflection 26.803.2224
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/README.md +13 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +966 -0
- package/dist/reflection/annotations.d.ts +15 -0
- package/dist/reflection/annotations.d.ts.map +1 -0
- package/dist/reflection/compact-metadata.d.ts +18 -0
- package/dist/reflection/compact-metadata.d.ts.map +1 -0
- package/dist/reflection/conversion.d.ts +15 -0
- package/dist/reflection/conversion.d.ts.map +1 -0
- package/dist/reflection/deserializer.d.ts +15 -0
- package/dist/reflection/deserializer.d.ts.map +1 -0
- package/dist/reflection/errors.d.ts +8 -0
- package/dist/reflection/errors.d.ts.map +1 -0
- package/dist/reflection/index.d.ts +10 -0
- package/dist/reflection/index.d.ts.map +1 -0
- package/dist/reflection/metadata-store.d.ts +20 -0
- package/dist/reflection/metadata-store.d.ts.map +1 -0
- package/dist/reflection/model.d.ts +273 -0
- package/dist/reflection/model.d.ts.map +1 -0
- package/dist/reflection/primitive-conversion.d.ts +2 -0
- package/dist/reflection/primitive-conversion.d.ts.map +1 -0
- package/dist/reflection/reflection-class.d.ts +60 -0
- package/dist/reflection/reflection-class.d.ts.map +1 -0
- package/dist/reflection/type-utils.d.ts +34 -0
- package/dist/reflection/type-utils.d.ts.map +1 -0
- package/dist/type-compiler/download-prebuilt.cjs +114 -0
- package/dist/type-compiler/go/ast_expression.go +187 -0
- package/dist/type-compiler/go/ast_metadata.go +388 -0
- package/dist/type-compiler/go/collect.go +963 -0
- package/dist/type-compiler/go/compact_metadata.go +553 -0
- package/dist/type-compiler/go/emission_plan.go +340 -0
- package/dist/type-compiler/go/emit_ast.go +557 -0
- package/dist/type-compiler/go/emit_ast_test.go +558 -0
- package/dist/type-compiler/go/go.mod +10 -0
- package/dist/type-compiler/go/plugin.go +359 -0
- package/dist/type-compiler/go/plugin_test.go +1206 -0
- package/dist/type-compiler/go/precompute.go +86 -0
- package/dist/type-compiler/go/receive_type.go +912 -0
- package/dist/type-compiler/go/resolve.go +265 -0
- package/dist/type-compiler/go/source_scan.go +51 -0
- package/dist/type-compiler/go/text_parse.go +734 -0
- package/dist/type-compiler/go/type_expr.go +1291 -0
- package/dist/type-compiler/go/typia_expr.go +2316 -0
- package/dist/type-compiler/index.cjs +43 -0
- package/dist/type-compiler/pnp.cjs +474 -0
- package/dist/type-compiler/prebuilt.cjs +324 -0
- package/dist/type-metadata-runtime.cjs +1 -0
- package/dist/type-metadata-runtime.d.ts +2 -0
- package/dist/type-metadata-runtime.d.ts.map +1 -0
- package/dist/type-metadata-runtime.js +107 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/primitives.d.ts +33 -0
- package/dist/types/primitives.d.ts.map +1 -0
- package/dist/types/runtime.d.ts +2 -0
- package/dist/types/runtime.d.ts.map +1 -0
- package/dist/types/type-annotations.d.ts +28 -0
- package/dist/types/type-annotations.d.ts.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
9
|
+
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
10
|
+
shimcore "github.com/microsoft/typescript-go/shim/core"
|
|
11
|
+
shimparser "github.com/microsoft/typescript-go/shim/parser"
|
|
12
|
+
shimprinter "github.com/microsoft/typescript-go/shim/printer"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func TestExpressionTemplateParsesStructuredMetadata(t *testing.T) {
|
|
16
|
+
template, err := parseExpressionTemplate(`{
|
|
17
|
+
kind: 16,
|
|
18
|
+
classType: () => Date,
|
|
19
|
+
value: (() => ({nested: [true, 1, "two"]}))()
|
|
20
|
+
}`)
|
|
21
|
+
if err != nil {
|
|
22
|
+
t.Fatal(err)
|
|
23
|
+
}
|
|
24
|
+
if template.parsed == nil || template.parsed.Kind != shimast.KindParenthesizedExpression {
|
|
25
|
+
t.Fatalf("parsed expression kind = %v", template.parsed)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestExpressionTemplateRejectsMalformedSourceAndPlaceholders(t *testing.T) {
|
|
30
|
+
for _, source := range []string{
|
|
31
|
+
`{kind:`,
|
|
32
|
+
`__tsf_runtime_import__("pkg", "Value")`,
|
|
33
|
+
`__tsf_runtime_namespace__("pkg", 1)`,
|
|
34
|
+
} {
|
|
35
|
+
if _, err := parseExpressionTemplate(source); err == nil {
|
|
36
|
+
t.Fatalf("parseExpressionTemplate(%q) unexpectedly succeeded", source)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func TestRuntimePlaceholderMaterializationPreservesModuleSemantics(t *testing.T) {
|
|
42
|
+
template, err := parseExpressionTemplate(`{
|
|
43
|
+
member: __tsf_runtime_import__("./dependency", "Value", "/project/dependency.ts"),
|
|
44
|
+
namespace: __tsf_runtime_namespace__("@scope/shared", "")
|
|
45
|
+
}`)
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for _, test := range []struct {
|
|
51
|
+
name string
|
|
52
|
+
commonJS bool
|
|
53
|
+
wantImports int
|
|
54
|
+
wantRequire bool
|
|
55
|
+
}{
|
|
56
|
+
{name: "esm", wantImports: 2},
|
|
57
|
+
{name: "commonjs", commonJS: true, wantRequire: true},
|
|
58
|
+
} {
|
|
59
|
+
t.Run(test.name, func(t *testing.T) {
|
|
60
|
+
ec := shimprinter.NewEmitContext()
|
|
61
|
+
file := parseTestSourceFile(t, "/project/consumer.ts", `export const value = 1;`)
|
|
62
|
+
imports := newAstImportRegistry(ec, file, test.commonJS)
|
|
63
|
+
materialized := template.materialize(ec, imports)
|
|
64
|
+
if containsRuntimePlaceholder(materialized) {
|
|
65
|
+
t.Fatal("compiler placeholder survived materialization")
|
|
66
|
+
}
|
|
67
|
+
if got := len(imports.statements()); got != test.wantImports {
|
|
68
|
+
t.Fatalf("generated imports = %d, want %d", got, test.wantImports)
|
|
69
|
+
}
|
|
70
|
+
if got := astContainsIdentifier(materialized, "require"); got != test.wantRequire {
|
|
71
|
+
t.Fatalf("contains require = %v, want %v", got, test.wantRequire)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func TestUpdateMetadataCallUsesAbsoluteArgumentSlot(t *testing.T) {
|
|
78
|
+
ec := shimprinter.NewEmitContext()
|
|
79
|
+
imports := newAstImportRegistry(ec, nil, true)
|
|
80
|
+
template, err := parseExpressionTemplate(`{kind: 7}`)
|
|
81
|
+
if err != nil {
|
|
82
|
+
t.Fatal(err)
|
|
83
|
+
}
|
|
84
|
+
call := ec.Factory.NewCallExpression(
|
|
85
|
+
ec.Factory.NewIdentifier("cast"),
|
|
86
|
+
nil,
|
|
87
|
+
nil,
|
|
88
|
+
ec.Factory.NewNodeList([]*shimast.Node{ec.Factory.NewIdentifier("input")}),
|
|
89
|
+
shimast.NodeFlagsNone,
|
|
90
|
+
).AsCallExpression()
|
|
91
|
+
updated := updateMetadataCall(
|
|
92
|
+
ec,
|
|
93
|
+
call,
|
|
94
|
+
callEmissionPlan{metadataArgIndex: 4, metadata: template},
|
|
95
|
+
imports,
|
|
96
|
+
newCompactMetadataRuntimeInterner(ec, nil),
|
|
97
|
+
"",
|
|
98
|
+
).AsCallExpression()
|
|
99
|
+
if got := len(updated.Arguments.Nodes); got != 5 {
|
|
100
|
+
t.Fatalf("arguments = %d, want 5", got)
|
|
101
|
+
}
|
|
102
|
+
for index := 1; index < 4; index++ {
|
|
103
|
+
if updated.Arguments.Nodes[index].Kind != shimast.KindIdentifier || updated.Arguments.Nodes[index].Text() != "undefined" {
|
|
104
|
+
t.Fatalf("argument %d was not an undefined filler", index)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func TestMetadataTransformMatchesExactCallPosition(t *testing.T) {
|
|
110
|
+
file := parseTestSourceFile(t, "/project/calls.ts", `
|
|
111
|
+
function same<T>(): void {}
|
|
112
|
+
same<number>();
|
|
113
|
+
same<string>();
|
|
114
|
+
`)
|
|
115
|
+
calls := collectAstNodes(file.AsNode(), shimast.KindCallExpression)
|
|
116
|
+
if len(calls) != 2 {
|
|
117
|
+
t.Fatalf("call expressions = %d, want 2", len(calls))
|
|
118
|
+
}
|
|
119
|
+
template, err := parseExpressionTemplate(`{kind: 7}`)
|
|
120
|
+
if err != nil {
|
|
121
|
+
t.Fatal(err)
|
|
122
|
+
}
|
|
123
|
+
plans := emissionPlans{file.FileName(): {
|
|
124
|
+
calls: map[int]callEmissionPlan{
|
|
125
|
+
calls[1].Pos(): {metadataArgIndex: 0, metadata: template},
|
|
126
|
+
},
|
|
127
|
+
classes: map[int]classEmissionPlan{},
|
|
128
|
+
commonJS: true,
|
|
129
|
+
}}
|
|
130
|
+
transformed := metadataTransform(plans)(shimprinter.NewEmitContext(), file)
|
|
131
|
+
transformedCalls := []*shimast.Node{}
|
|
132
|
+
for _, call := range collectAstNodes(transformed.AsNode(), shimast.KindCallExpression) {
|
|
133
|
+
expression := call.AsCallExpression().Expression
|
|
134
|
+
if expression != nil && expression.Kind == shimast.KindIdentifier && expression.Text() == "same" {
|
|
135
|
+
transformedCalls = append(transformedCalls, call)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if len(transformedCalls) != 2 {
|
|
139
|
+
t.Fatalf("transformed calls = %d, want 2", len(transformedCalls))
|
|
140
|
+
}
|
|
141
|
+
if len(transformedCalls[0].AsCallExpression().Arguments.Nodes) != 0 {
|
|
142
|
+
t.Fatal("unplanned same-named call was transformed")
|
|
143
|
+
}
|
|
144
|
+
if len(transformedCalls[1].AsCallExpression().Arguments.Nodes) != 1 {
|
|
145
|
+
t.Fatal("planned call was not transformed")
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func TestClassMetadataTransformMaterializesOneSharedObject(t *testing.T) {
|
|
150
|
+
file := parseTestSourceFile(t, "/project/model.ts", `class Model { value: string }`)
|
|
151
|
+
classes := collectAstNodes(file.AsNode(), shimast.KindClassDeclaration)
|
|
152
|
+
if len(classes) != 1 {
|
|
153
|
+
t.Fatalf("class declarations = %d, want 1", len(classes))
|
|
154
|
+
}
|
|
155
|
+
template, err := parseExpressionTemplate(`{kind: 16, properties: []}`)
|
|
156
|
+
if err != nil {
|
|
157
|
+
t.Fatal(err)
|
|
158
|
+
}
|
|
159
|
+
plans := emissionPlans{file.FileName(): {
|
|
160
|
+
calls: map[int]callEmissionPlan{},
|
|
161
|
+
classes: map[int]classEmissionPlan{
|
|
162
|
+
classes[0].Pos(): {name: "Model", metadata: template},
|
|
163
|
+
},
|
|
164
|
+
commonJS: true,
|
|
165
|
+
}}
|
|
166
|
+
transformed := metadataTransform(plans)(shimprinter.NewEmitContext(), file)
|
|
167
|
+
if got := len(collectAstNodes(transformed.AsNode(), shimast.KindObjectLiteralExpression)); got != 0 {
|
|
168
|
+
t.Fatalf("class metadata left %d object literals in the emitted AST", got)
|
|
169
|
+
}
|
|
170
|
+
if !astContainsIdentifier(transformed.AsNode(), compactMetadataDecoderName) {
|
|
171
|
+
t.Fatal("class metadata was not decoded from compact data")
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func TestPureJSONAliasMetadataDoesNotLoadTheRuntimeDecoder(t *testing.T) {
|
|
176
|
+
file := parseTestSourceFile(t, "/project/shared.ts", `export type Status = "ready" | "busy"`)
|
|
177
|
+
template, err := parseExpressionTemplate(`{Status: {kind: 12, types: [{kind: 10, literal: "ready"}, {kind: 10, literal: "busy"}]}}`)
|
|
178
|
+
if err != nil {
|
|
179
|
+
t.Fatal(err)
|
|
180
|
+
}
|
|
181
|
+
plans := emissionPlans{file.FileName(): {
|
|
182
|
+
calls: map[int]callEmissionPlan{},
|
|
183
|
+
classes: map[int]classEmissionPlan{},
|
|
184
|
+
aliases: &template,
|
|
185
|
+
commonJS: true,
|
|
186
|
+
}}
|
|
187
|
+
|
|
188
|
+
transformed := metadataTransform(plans)(shimprinter.NewEmitContext(), file)
|
|
189
|
+
if astContainsIdentifier(transformed.AsNode(), compactMetadataDecoderName) {
|
|
190
|
+
t.Fatal("pure JSON aliases loaded the compact metadata runtime")
|
|
191
|
+
}
|
|
192
|
+
if !astContainsIdentifier(transformed.AsNode(), "JSON") || !astContainsIdentifier(transformed.AsNode(), "parse") {
|
|
193
|
+
t.Fatal("pure JSON aliases were not decoded with JSON.parse")
|
|
194
|
+
}
|
|
195
|
+
if got := len(collectAstNodes(transformed.AsNode(), shimast.KindImportDeclaration)); got != 0 {
|
|
196
|
+
t.Fatalf("generated imports = %d, want none", got)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func TestAliasMetadataWithRuntimeValuesStillLoadsTheRuntimeDecoder(t *testing.T) {
|
|
201
|
+
file := parseTestSourceFile(t, "/project/shared.ts", `export class Model {}; export type Wrapped = { value: Model }`)
|
|
202
|
+
template, err := parseExpressionTemplate(`{Wrapped: {kind: 16, properties: [{key: "value", value: {kind: 16, classType: () => Model}}]}}`)
|
|
203
|
+
if err != nil {
|
|
204
|
+
t.Fatal(err)
|
|
205
|
+
}
|
|
206
|
+
plans := emissionPlans{file.FileName(): {
|
|
207
|
+
calls: map[int]callEmissionPlan{},
|
|
208
|
+
classes: map[int]classEmissionPlan{},
|
|
209
|
+
aliases: &template,
|
|
210
|
+
commonJS: true,
|
|
211
|
+
}}
|
|
212
|
+
|
|
213
|
+
transformed := metadataTransform(plans)(shimprinter.NewEmitContext(), file)
|
|
214
|
+
if !astContainsIdentifier(transformed.AsNode(), compactMetadataDecoderName) {
|
|
215
|
+
t.Fatal("alias metadata with runtime values did not load the compact metadata runtime")
|
|
216
|
+
}
|
|
217
|
+
if got := len(collectAstNodes(transformed.AsNode(), shimast.KindImportDeclaration)); got != 1 {
|
|
218
|
+
t.Fatalf("generated imports = %d, want one decoder import", got)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func TestMetadataRuntimeRequirementAllowsPureJSONAliases(t *testing.T) {
|
|
223
|
+
template, err := parseExpressionTemplate(`{Status: {kind: 12, types: [{kind: 10, literal: "ready"}, {kind: 10, literal: "busy"}]}}`)
|
|
224
|
+
if err != nil {
|
|
225
|
+
t.Fatal(err)
|
|
226
|
+
}
|
|
227
|
+
plan := &fileEmissionPlan{
|
|
228
|
+
calls: map[int]callEmissionPlan{},
|
|
229
|
+
classes: map[int]classEmissionPlan{},
|
|
230
|
+
aliases: &template,
|
|
231
|
+
commonJS: true,
|
|
232
|
+
}
|
|
233
|
+
if got := metadataRuntimeRequirement(plan); got != "" {
|
|
234
|
+
t.Fatalf("pure JSON aliases require metadata runtime: %s", got)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func TestMetadataRuntimeRequirementRejectsRuntimeAliasValues(t *testing.T) {
|
|
239
|
+
template, err := parseExpressionTemplate(`{RuntimeValue: {kind: 16, classType: () => Date}}`)
|
|
240
|
+
if err != nil {
|
|
241
|
+
t.Fatal(err)
|
|
242
|
+
}
|
|
243
|
+
plan := &fileEmissionPlan{
|
|
244
|
+
calls: map[int]callEmissionPlan{},
|
|
245
|
+
classes: map[int]classEmissionPlan{},
|
|
246
|
+
aliases: &template,
|
|
247
|
+
}
|
|
248
|
+
if got := metadataRuntimeRequirement(plan); got != "reflected alias metadata for RuntimeValue" {
|
|
249
|
+
t.Fatalf("runtime alias requirement = %q", got)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func TestMetadataRuntimeRequirementRejectsRuntimeMetadataSurfaces(t *testing.T) {
|
|
254
|
+
template, err := parseExpressionTemplate(`{kind: 6}`)
|
|
255
|
+
if err != nil {
|
|
256
|
+
t.Fatal(err)
|
|
257
|
+
}
|
|
258
|
+
tests := []struct {
|
|
259
|
+
name string
|
|
260
|
+
plan *fileEmissionPlan
|
|
261
|
+
want string
|
|
262
|
+
}{
|
|
263
|
+
{
|
|
264
|
+
name: "class",
|
|
265
|
+
plan: &fileEmissionPlan{classes: map[int]classEmissionPlan{1: {name: "Model", metadata: template}}},
|
|
266
|
+
want: "reflected class metadata",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "call",
|
|
270
|
+
plan: &fileEmissionPlan{calls: map[int]callEmissionPlan{1: {name: "typeOf", metadata: template}}},
|
|
271
|
+
want: "reflected call metadata",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "registry",
|
|
275
|
+
plan: &fileEmissionPlan{metadataTypes: []expressionTemplate{template}},
|
|
276
|
+
want: "the reflected metadata registry",
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
for _, test := range tests {
|
|
280
|
+
t.Run(test.name, func(t *testing.T) {
|
|
281
|
+
if got := metadataRuntimeRequirement(test.plan); got != test.want {
|
|
282
|
+
t.Fatalf("metadata runtime requirement = %q, want %q", got, test.want)
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
func TestCompactMetadataHidesGeneratedObjectFromBuiltinTransforms(t *testing.T) {
|
|
289
|
+
file := parseTestSourceFile(t, "/project/model.ts", `class Model {}`)
|
|
290
|
+
template, err := parseExpressionTemplate(`{kind: 16, classType: () => Model}`)
|
|
291
|
+
if err != nil {
|
|
292
|
+
t.Fatal(err)
|
|
293
|
+
}
|
|
294
|
+
plans := emissionPlans{file.FileName(): {
|
|
295
|
+
calls: map[int]callEmissionPlan{},
|
|
296
|
+
classes: map[int]classEmissionPlan{},
|
|
297
|
+
metadataTypes: []expressionTemplate{template},
|
|
298
|
+
metadataTypeResolver: "__tsf_metadata_type",
|
|
299
|
+
commonJS: true,
|
|
300
|
+
}}
|
|
301
|
+
|
|
302
|
+
transformed := metadataTransform(plans)(shimprinter.NewEmitContext(), file)
|
|
303
|
+
if got := len(collectAstNodes(transformed.AsNode(), shimast.KindObjectLiteralExpression)); got != 0 {
|
|
304
|
+
t.Fatalf("compact metadata left %d object literals in the emitted AST", got)
|
|
305
|
+
}
|
|
306
|
+
if astContainsIdentifier(transformed.AsNode(), "eval") {
|
|
307
|
+
t.Fatal("compact metadata emitted direct eval")
|
|
308
|
+
}
|
|
309
|
+
if !astContainsIdentifier(transformed.AsNode(), compactMetadataRegistryName) {
|
|
310
|
+
t.Fatal("compact metadata did not create a lazy runtime registry")
|
|
311
|
+
}
|
|
312
|
+
if got := len(collectAstNodes(transformed.AsNode(), shimast.KindImportDeclaration)); got != 1 {
|
|
313
|
+
t.Fatalf("generated imports = %d, want one decoder import", got)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
func TestCompactMetadataEncoderExtractsRuntimeExpressions(t *testing.T) {
|
|
318
|
+
template, err := parseExpressionTemplate(`{
|
|
319
|
+
kind: 16,
|
|
320
|
+
classType: () => Model,
|
|
321
|
+
missing: undefined,
|
|
322
|
+
big: 12n,
|
|
323
|
+
nested: [true, -2, "two"]
|
|
324
|
+
}`)
|
|
325
|
+
if err != nil {
|
|
326
|
+
t.Fatal(err)
|
|
327
|
+
}
|
|
328
|
+
encoding := encodeCompactMetadata(template.parsed)
|
|
329
|
+
if got := len(encoding.references); got != 3 {
|
|
330
|
+
t.Fatalf("runtime references = %d, want 3", got)
|
|
331
|
+
}
|
|
332
|
+
for _, want := range []string{
|
|
333
|
+
`[1,{"kind":16`,
|
|
334
|
+
`"classType":{"$tsf":0}`,
|
|
335
|
+
`"missing":{"$tsf":1}`,
|
|
336
|
+
`"big":{"$tsf":2}`,
|
|
337
|
+
`"nested":[true,-2,"two"]`,
|
|
338
|
+
} {
|
|
339
|
+
if !strings.Contains(encoding.serialized, want) {
|
|
340
|
+
t.Fatalf("compact payload is missing %q: %s", want, encoding.serialized)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func TestCompactMetadataEncoderEscapesReferenceMarkerCollision(t *testing.T) {
|
|
346
|
+
for _, source := range []string{
|
|
347
|
+
`{"$tsf": 7}`,
|
|
348
|
+
`{"$tsfImport": [0, "Model"]}`,
|
|
349
|
+
`{"$tsfAlias": [0, "Alias", "Alias"]}`,
|
|
350
|
+
`{"$tsfType": 0}`,
|
|
351
|
+
} {
|
|
352
|
+
template, err := parseExpressionTemplate(source)
|
|
353
|
+
if err != nil {
|
|
354
|
+
t.Fatal(err)
|
|
355
|
+
}
|
|
356
|
+
encoding := encodeCompactMetadata(template.parsed)
|
|
357
|
+
if encoding.serialized != `[1,{"$tsf":0}]` {
|
|
358
|
+
t.Fatalf("compact payload for %s = %s", source, encoding.serialized)
|
|
359
|
+
}
|
|
360
|
+
if len(encoding.references) != 1 || encoding.references[0].Kind != shimast.KindObjectLiteralExpression {
|
|
361
|
+
t.Fatalf("marker-shaped application data %s was not escaped through a runtime slot", source)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func TestCompactMetadataEncodesCommonJSImportsAndAliasesAsRecipes(t *testing.T) {
|
|
367
|
+
file := parseTestSourceFile(t, "/project/consumer.ts", `export const value = 1`)
|
|
368
|
+
template, err := parseExpressionTemplate(`{
|
|
369
|
+
classType: () => __tsf_runtime_import__("./model", "Model", "/project/model.ts"),
|
|
370
|
+
alias: __tsf_runtime_alias__("@scope/types", "Alias", "RenamedAlias")
|
|
371
|
+
}`)
|
|
372
|
+
if err != nil {
|
|
373
|
+
t.Fatal(err)
|
|
374
|
+
}
|
|
375
|
+
ec := shimprinter.NewEmitContext()
|
|
376
|
+
imports := newAstImportRegistry(ec, file, true)
|
|
377
|
+
interner := newCompactMetadataRuntimeInterner(ec, file)
|
|
378
|
+
materialized := template.materialize(ec, imports)
|
|
379
|
+
encoding := encodeCompactMetadataWithRuntimeRecipes(
|
|
380
|
+
materialized,
|
|
381
|
+
interner.deduplicationKey,
|
|
382
|
+
func(encoder *compactMetadataEncoder, expression *shimast.Node) bool {
|
|
383
|
+
return writeCompactMetadataRuntimeRecipe(encoder, expression, imports, "")
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
if !strings.Contains(encoding.serialized, `"classType":{"$tsfImport":[0,"./model","Model"]}`) {
|
|
387
|
+
t.Fatalf("class import was not encoded as a recipe: %s", encoding.serialized)
|
|
388
|
+
}
|
|
389
|
+
if !strings.Contains(encoding.serialized, `"alias":{"$tsfAlias":[0,"@scope/types","Alias","RenamedAlias"]}`) {
|
|
390
|
+
t.Fatalf("alias was not encoded as a recipe: %s", encoding.serialized)
|
|
391
|
+
}
|
|
392
|
+
if len(encoding.references) != 1 {
|
|
393
|
+
t.Fatalf("module loader references = %d, want 1", len(encoding.references))
|
|
394
|
+
}
|
|
395
|
+
for _, reference := range encoding.references {
|
|
396
|
+
if !isCompactMetadataCommonJSRequire(reference, imports) {
|
|
397
|
+
t.Fatal("recipe emitted a per-type runtime expression instead of the file's require function")
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
func TestCompactMetadataEncoderDeduplicatesGeneratedRuntimeThunks(t *testing.T) {
|
|
403
|
+
file := parseTestSourceFile(t, "/project/model.ts", `class Model {}`)
|
|
404
|
+
template, err := parseExpressionTemplate(`{first: () => Model, second: () => Model}`)
|
|
405
|
+
if err != nil {
|
|
406
|
+
t.Fatal(err)
|
|
407
|
+
}
|
|
408
|
+
ec := shimprinter.NewEmitContext()
|
|
409
|
+
interner := newCompactMetadataRuntimeInterner(ec, file)
|
|
410
|
+
metadata := template.materialize(ec, newAstImportRegistry(ec, file, true))
|
|
411
|
+
encoding := encodeCompactMetadataWithReferenceKeys(metadata, interner.deduplicationKey)
|
|
412
|
+
if len(encoding.references) != 1 {
|
|
413
|
+
t.Fatalf("runtime references = %d, want 1", len(encoding.references))
|
|
414
|
+
}
|
|
415
|
+
if strings.Count(encoding.serialized, `{"$tsf":0}`) != 2 {
|
|
416
|
+
t.Fatalf("repeated thunk did not reuse one reference index: %s", encoding.serialized)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
func TestMetadataTypeInternerKeepsRuntimeExpressionsAtUseSite(t *testing.T) {
|
|
421
|
+
interner := newMetadataTypeInterner("")
|
|
422
|
+
runtime := `{kind: 16, classType: () => LocalModel}`
|
|
423
|
+
if got := interner.reference(runtime); got != runtime {
|
|
424
|
+
t.Fatalf("runtime metadata was moved to module scope: %s", got)
|
|
425
|
+
}
|
|
426
|
+
if len(interner.expressions) != 0 {
|
|
427
|
+
t.Fatalf("shared runtime metadata expressions = %d, want 0", len(interner.expressions))
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if got := interner.reference(`{kind: 6}`); got != "__tsf_metadata_type(0)" {
|
|
431
|
+
t.Fatalf("JSON metadata reference = %s", got)
|
|
432
|
+
}
|
|
433
|
+
if len(interner.expressions) != 1 {
|
|
434
|
+
t.Fatalf("shared JSON metadata expressions = %d, want 1", len(interner.expressions))
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
func TestCompactMetadataTypeRecipeAcceptsParsedTemplate(t *testing.T) {
|
|
439
|
+
template, err := parseExpressionTemplate(`__tsf_metadata_type(3)`)
|
|
440
|
+
if err != nil {
|
|
441
|
+
t.Fatal(err)
|
|
442
|
+
}
|
|
443
|
+
index, ok := compactMetadataTypeRecipe(template.parsed, "__tsf_metadata_type")
|
|
444
|
+
if !ok || index != 3 {
|
|
445
|
+
t.Fatalf("metadata type recipe = (%d, %t), want (3, true)", index, ok)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
func TestCompactMetadataEncoderRollsBackUnsupportedNestedValues(t *testing.T) {
|
|
450
|
+
file := parseTestSourceFile(t, "/project/model.ts", `class Model {}`)
|
|
451
|
+
template, err := parseExpressionTemplate(`{
|
|
452
|
+
escaped: {value: () => Model, ...extra},
|
|
453
|
+
again: () => Model
|
|
454
|
+
}`)
|
|
455
|
+
if err != nil {
|
|
456
|
+
t.Fatal(err)
|
|
457
|
+
}
|
|
458
|
+
interner := newCompactMetadataRuntimeInterner(shimprinter.NewEmitContext(), file)
|
|
459
|
+
encoding := encodeCompactMetadataWithReferenceKeys(template.parsed, interner.deduplicationKey)
|
|
460
|
+
if encoding.serialized != `[1,{"escaped":{"$tsf":0},"again":{"$tsf":1}}]` {
|
|
461
|
+
t.Fatalf("compact payload retained a stale nested reference: %s", encoding.serialized)
|
|
462
|
+
}
|
|
463
|
+
if len(encoding.references) != 2 {
|
|
464
|
+
t.Fatalf("runtime references = %d, want 2", len(encoding.references))
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
func TestClassFromNodeRecognizesAmbientContext(t *testing.T) {
|
|
469
|
+
file := parseTestSourceFile(t, "/project/ambient.ts", `
|
|
470
|
+
declare namespace AmbientScope {
|
|
471
|
+
class Nested { value: string }
|
|
472
|
+
}
|
|
473
|
+
`)
|
|
474
|
+
classes := collectAstNodes(file.AsNode(), shimast.KindClassDeclaration)
|
|
475
|
+
if len(classes) != 1 {
|
|
476
|
+
t.Fatalf("class declarations = %d, want 1", len(classes))
|
|
477
|
+
}
|
|
478
|
+
info := &fileInfo{file: file}
|
|
479
|
+
class := classFromNode(info, classes[0])
|
|
480
|
+
if class == nil || !class.ambient {
|
|
481
|
+
t.Fatal("class nested in an ambient namespace was not marked ambient")
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
func TestPrecomputeSkipsAmbientClasses(t *testing.T) {
|
|
486
|
+
file := parseTestSourceFile(t, "/project/precompute.ts", ``)
|
|
487
|
+
ambient := &classInfo{ambient: true, properties: []propertyInfo{{typeText: "string"}}}
|
|
488
|
+
runtimeClass := &classInfo{properties: []propertyInfo{{typeText: "string"}}}
|
|
489
|
+
info := &fileInfo{
|
|
490
|
+
file: file,
|
|
491
|
+
moduleKey: moduleKey(file.FileName()),
|
|
492
|
+
precompute: true,
|
|
493
|
+
aliases: map[string]aliasInfo{},
|
|
494
|
+
interfaces: map[string][]interfaceInfo{},
|
|
495
|
+
enums: map[string]enumInfo{},
|
|
496
|
+
classes: []*classInfo{ambient, runtimeClass},
|
|
497
|
+
imports: map[string]importRef{},
|
|
498
|
+
reexports: map[string]importRef{},
|
|
499
|
+
}
|
|
500
|
+
reg := ®istry{
|
|
501
|
+
files: map[string]*fileInfo{file.FileName(): info},
|
|
502
|
+
byPath: map[string]*fileInfo{info.moduleKey: info},
|
|
503
|
+
typiaCache: map[typiaCacheKey]string{},
|
|
504
|
+
typiaFailures: map[*shimchecker.Type]bool{},
|
|
505
|
+
classes: map[string]*classInfo{},
|
|
506
|
+
external: map[string]map[string][]functionInfo{},
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
precomputeMetadataExpressions(reg, true)
|
|
510
|
+
|
|
511
|
+
if ambient.properties[0].metadataText != "" {
|
|
512
|
+
t.Fatal("ambient class metadata was precomputed")
|
|
513
|
+
}
|
|
514
|
+
if runtimeClass.properties[0].metadataText == "" {
|
|
515
|
+
t.Fatal("runtime class metadata was not precomputed")
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
func parseTestSourceFile(t *testing.T, fileName string, source string) *shimast.SourceFile {
|
|
520
|
+
t.Helper()
|
|
521
|
+
file := shimparser.ParseSourceFile(
|
|
522
|
+
shimast.SourceFileParseOptions{FileName: filepath.ToSlash(fileName)},
|
|
523
|
+
source,
|
|
524
|
+
shimcore.ScriptKindTS,
|
|
525
|
+
)
|
|
526
|
+
if file == nil || len(file.Diagnostics()) != 0 {
|
|
527
|
+
t.Fatalf("failed to parse test source %s", fileName)
|
|
528
|
+
}
|
|
529
|
+
return file
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
func collectAstNodes(root *shimast.Node, kind shimast.Kind) []*shimast.Node {
|
|
533
|
+
out := []*shimast.Node{}
|
|
534
|
+
var walk func(*shimast.Node)
|
|
535
|
+
walk = func(node *shimast.Node) {
|
|
536
|
+
if node == nil {
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
if node.Kind == kind {
|
|
540
|
+
out = append(out, node)
|
|
541
|
+
}
|
|
542
|
+
node.ForEachChild(func(child *shimast.Node) bool {
|
|
543
|
+
walk(child)
|
|
544
|
+
return false
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
walk(root)
|
|
548
|
+
return out
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
func astContainsIdentifier(root *shimast.Node, name string) bool {
|
|
552
|
+
for _, node := range collectAstNodes(root, shimast.KindIdentifier) {
|
|
553
|
+
if node.Text() == name {
|
|
554
|
+
return true
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false
|
|
558
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module tsf-type-replacement-plugin
|
|
2
|
+
|
|
3
|
+
go 1.26
|
|
4
|
+
|
|
5
|
+
require (
|
|
6
|
+
// ttsc replaces this placeholder with the installed npm package's Go sources
|
|
7
|
+
// while building source plugins. The nested Go module has no semver tags.
|
|
8
|
+
github.com/samchon/ttsc/packages/ttsc v0.0.0
|
|
9
|
+
github.com/samchon/typia/packages/typia/native v0.0.0-20260709031533-27f105ae8e56
|
|
10
|
+
)
|