@tsonic/cli 0.0.72 → 0.0.73

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.
@@ -61,8 +61,64 @@ const runLibraryBuild = (dir, wsConfigPath) => {
61
61
  const result = spawnSync("node", [cliPath, "build", "--project", "lib", "--config", wsConfigPath, "--quiet"], { cwd: dir, encoding: "utf-8" });
62
62
  expect(result.status, result.stderr || result.stdout).to.equal(0);
63
63
  };
64
+ const runProjectBuild = (dir, wsConfigPath, projectName) => {
65
+ const cliPath = join(repoRoot, "packages/cli/dist/index.js");
66
+ const result = spawnSync("node", [
67
+ cliPath,
68
+ "build",
69
+ "--project",
70
+ projectName,
71
+ "--config",
72
+ wsConfigPath,
73
+ "--quiet",
74
+ ], { cwd: dir, encoding: "utf-8" });
75
+ expect(result.status, result.stderr || result.stdout).to.equal(0);
76
+ };
64
77
  describe("library bindings first-party regressions", function () {
65
78
  this.timeout(10 * 60 * 1000);
79
+ it("emits canonical binding alias markers for nominal source-binding types", () => {
80
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-canonical-alias-"));
81
+ try {
82
+ const wsConfigPath = writeLibraryScaffold(dir, "Test.Lib", "Test.Lib");
83
+ writeFileSync(join(dir, "packages", "lib", "src", "index.ts"), [
84
+ "export class Attachment {",
85
+ " Id: string = \"\";",
86
+ "}",
87
+ "",
88
+ ].join("\n"), "utf-8");
89
+ runLibraryBuild(dir, wsConfigPath);
90
+ const internal = readFileSync(join(dir, "packages", "lib", "dist", "tsonic", "bindings", "Test.Lib", "internal", "index.d.ts"), "utf-8");
91
+ expect(internal).to.include('readonly "__tsonic_binding_alias_Test.Lib.Attachment"?: never;');
92
+ }
93
+ finally {
94
+ rmSync(dir, { recursive: true, force: true });
95
+ }
96
+ });
97
+ it("emits canonical manifest aliases for generic source-binding types", () => {
98
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-generic-alias-"));
99
+ try {
100
+ const wsConfigPath = writeLibraryScaffold(dir, "Test.Lib", "Test.Lib");
101
+ writeFileSync(join(dir, "packages", "lib", "src", "index.ts"), [
102
+ "export type Result<T> = {",
103
+ " ok: T;",
104
+ "};",
105
+ "",
106
+ "export class Box<T> {",
107
+ " value!: T;",
108
+ "}",
109
+ "",
110
+ ].join("\n"), "utf-8");
111
+ runLibraryBuild(dir, wsConfigPath);
112
+ const bindings = JSON.parse(readFileSync(join(dir, "packages", "lib", "dist", "tsonic", "bindings", "Test.Lib", "bindings.json"), "utf-8"));
113
+ expect(bindings.types?.some((type) => type.clrName === "Test.Lib.Result__Alias`1" &&
114
+ type.alias === "Test.Lib.Result__Alias_1")).to.equal(true);
115
+ expect(bindings.types?.some((type) => type.clrName === "Test.Lib.Box`1" &&
116
+ type.alias === "Test.Lib.Box_1")).to.equal(true);
117
+ }
118
+ finally {
119
+ rmSync(dir, { recursive: true, force: true });
120
+ }
121
+ });
66
122
  it("uses declaring-module source signatures for transitive re-exported function values", () => {
67
123
  const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-declaring-"));
68
124
  try {
@@ -95,6 +151,7 @@ describe("library bindings first-party regressions", function () {
95
151
  .split("\n")
96
152
  .find((line) => line.includes("export declare function ok<"));
97
153
  expect(okLine).to.not.equal(undefined);
154
+ expect(facade).to.include("import type { Success } from './Test.Lib.types.js';");
98
155
  expect(okLine ?? "").to.match(/:\s*Success<\s*T\s*>;/);
99
156
  expect(okLine ?? "").to.not.match(/\$instance|__\d+\b/);
100
157
  }
@@ -102,6 +159,131 @@ describe("library bindings first-party regressions", function () {
102
159
  rmSync(dir, { recursive: true, force: true });
103
160
  }
104
161
  });
162
+ it("preserves declaring-namespace identity for non-exported helper types on re-exported functions", () => {
163
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-reexported-helper-"));
164
+ try {
165
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
166
+ mkdirSync(join(dir, "packages", "messages", "src", "domain"), {
167
+ recursive: true,
168
+ });
169
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
170
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
171
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
172
+ name: "test",
173
+ private: true,
174
+ type: "module",
175
+ workspaces: ["packages/*"],
176
+ }, null, 2) + "\n", "utf-8");
177
+ writeFileSync(wsConfigPath, JSON.stringify({
178
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
179
+ dotnetVersion: "net10.0",
180
+ }, null, 2) + "\n", "utf-8");
181
+ writeFileSync(join(dir, "packages", "messages", "package.json"), JSON.stringify({
182
+ name: "@acme/messages",
183
+ private: true,
184
+ type: "module",
185
+ exports: {
186
+ "./package.json": "./package.json",
187
+ "./*.js": {
188
+ types: "./dist/tsonic/bindings/*.d.ts",
189
+ default: "./dist/tsonic/bindings/*.js",
190
+ },
191
+ },
192
+ }, null, 2) + "\n", "utf-8");
193
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
194
+ name: "@acme/app",
195
+ private: true,
196
+ type: "module",
197
+ dependencies: {
198
+ "@acme/messages": "workspace:*",
199
+ },
200
+ }, null, 2) + "\n", "utf-8");
201
+ writeFileSync(join(dir, "packages", "messages", "tsonic.json"), JSON.stringify({
202
+ $schema: "https://tsonic.org/schema/v1.json",
203
+ rootNamespace: "Acme.Messages",
204
+ entryPoint: "src/index.ts",
205
+ sourceRoot: "src",
206
+ outputDirectory: "generated",
207
+ outputName: "Acme.Messages",
208
+ output: {
209
+ type: "library",
210
+ targetFrameworks: ["net10.0"],
211
+ nativeAot: false,
212
+ generateDocumentation: false,
213
+ includeSymbols: false,
214
+ packable: false,
215
+ },
216
+ }, null, 2) + "\n", "utf-8");
217
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
218
+ $schema: "https://tsonic.org/schema/v1.json",
219
+ rootNamespace: "Acme.App",
220
+ entryPoint: "src/App.ts",
221
+ sourceRoot: "src",
222
+ references: {
223
+ libraries: [
224
+ "../messages/generated/bin/Release/net10.0/Acme.Messages.dll",
225
+ ],
226
+ },
227
+ outputDirectory: "generated",
228
+ outputName: "Acme.App",
229
+ output: {
230
+ type: "library",
231
+ targetFrameworks: ["net10.0"],
232
+ nativeAot: false,
233
+ generateDocumentation: false,
234
+ includeSymbols: false,
235
+ packable: false,
236
+ },
237
+ }, null, 2) + "\n", "utf-8");
238
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
239
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
240
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
241
+ writeFileSync(join(dir, "packages", "messages", "src", "domain", "send-message.ts"), [
242
+ `interface SendMessageInput {`,
243
+ ` type: string;`,
244
+ ` to: string;`,
245
+ ` topic?: string;`,
246
+ ` content: string;`,
247
+ `}`,
248
+ ``,
249
+ `export const sendMessageDomain = async (params: SendMessageInput): Promise<{ id: string }> => {`,
250
+ ` return { id: params.to + ":" + params.content };`,
251
+ `};`,
252
+ ``,
253
+ ].join("\n"), "utf-8");
254
+ writeFileSync(join(dir, "packages", "messages", "src", "index.ts"), [`export { sendMessageDomain } from "./domain/send-message.ts";`, ``].join("\n"), "utf-8");
255
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
256
+ `import { sendMessageDomain } from "@acme/messages/Acme.Messages.js";`,
257
+ ``,
258
+ `export async function run(): Promise<string> {`,
259
+ ` const result = await sendMessageDomain({`,
260
+ ` type: "stream",`,
261
+ ` to: "general",`,
262
+ ` topic: "ops",`,
263
+ ` content: "hello",`,
264
+ ` });`,
265
+ ` return result.id;`,
266
+ `}`,
267
+ ``,
268
+ ].join("\n"), "utf-8");
269
+ runProjectBuild(dir, wsConfigPath, "messages");
270
+ linkDir(join(dir, "packages", "messages"), join(dir, "node_modules/@acme/messages"));
271
+ const bindingsRoot = join(dir, "packages", "messages", "dist", "tsonic", "bindings");
272
+ const rootInternal = readFileSync(join(bindingsRoot, "Acme.Messages", "internal", "index.d.ts"), "utf-8");
273
+ const rootBindings = JSON.parse(readFileSync(join(bindingsRoot, "Acme.Messages", "bindings.json"), "utf-8"));
274
+ expect(rootInternal).to.include('readonly "__tsonic_binding_alias_Acme.Messages.domain.SendMessageInput"?: never;');
275
+ expect(rootInternal).to.not.include('readonly "__tsonic_binding_alias_Acme.Messages.SendMessageInput"?: never;');
276
+ expect(rootBindings.types.map((item) => item.clrName)).to.not.include("Acme.Messages.SendMessageInput");
277
+ expect(rootBindings.types.map((item) => item.clrName)).to.include("Acme.Messages.domain.SendMessageInput");
278
+ runProjectBuild(dir, wsConfigPath, "app");
279
+ const appGenerated = readFileSync(join(dir, "packages", "app", "generated", "App.cs"), "utf-8");
280
+ expect(appGenerated).to.include("new global::Acme.Messages.domain.SendMessageInput");
281
+ expect(appGenerated).to.not.include("new global::Acme.Messages.SendMessageInput");
282
+ }
283
+ finally {
284
+ rmSync(dir, { recursive: true, force: true });
285
+ }
286
+ });
105
287
  it("emits boolean literal signatures as System.Boolean in bindings metadata", () => {
106
288
  const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-boollit-"));
107
289
  try {
@@ -210,5 +392,2243 @@ describe("library bindings first-party regressions", function () {
210
392
  rmSync(dir, { recursive: true, force: true });
211
393
  }
212
394
  });
395
+ it("preserves external and cross-namespace source type closure through generated library bindings", () => {
396
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-closure-"));
397
+ try {
398
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
399
+ mkdirSync(join(dir, "packages", "core", "src", "db"), {
400
+ recursive: true,
401
+ });
402
+ mkdirSync(join(dir, "packages", "core", "src", "entities"), {
403
+ recursive: true,
404
+ });
405
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
406
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
407
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
408
+ name: "test",
409
+ private: true,
410
+ type: "module",
411
+ workspaces: ["packages/*"],
412
+ }, null, 2) + "\n", "utf-8");
413
+ writeFileSync(wsConfigPath, JSON.stringify({
414
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
415
+ dotnetVersion: "net10.0",
416
+ dotnet: {
417
+ packageReferences: [
418
+ {
419
+ id: "Microsoft.EntityFrameworkCore",
420
+ version: "10.0.1",
421
+ types: "@tsonic/efcore",
422
+ },
423
+ ],
424
+ libraries: [],
425
+ frameworkReferences: [],
426
+ },
427
+ }, null, 2) + "\n", "utf-8");
428
+ writeFileSync(join(dir, "packages", "core", "package.json"), JSON.stringify({
429
+ name: "@acme/core",
430
+ private: true,
431
+ type: "module",
432
+ dependencies: {
433
+ "@tsonic/efcore": "*",
434
+ },
435
+ exports: {
436
+ "./package.json": "./package.json",
437
+ "./*.js": {
438
+ types: "./dist/tsonic/bindings/*.d.ts",
439
+ default: "./dist/tsonic/bindings/*.js",
440
+ },
441
+ },
442
+ }, null, 2) + "\n", "utf-8");
443
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
444
+ name: "@acme/app",
445
+ private: true,
446
+ type: "module",
447
+ dependencies: {
448
+ "@acme/core": "workspace:*",
449
+ },
450
+ }, null, 2) + "\n", "utf-8");
451
+ writeFileSync(join(dir, "packages", "core", "tsonic.json"), JSON.stringify({
452
+ $schema: "https://tsonic.org/schema/v1.json",
453
+ rootNamespace: "Acme.Core",
454
+ entryPoint: "src/index.ts",
455
+ sourceRoot: "src",
456
+ outputDirectory: "generated",
457
+ outputName: "Acme.Core",
458
+ output: {
459
+ type: "library",
460
+ targetFrameworks: ["net10.0"],
461
+ nativeAot: false,
462
+ generateDocumentation: false,
463
+ includeSymbols: false,
464
+ packable: false,
465
+ },
466
+ }, null, 2) + "\n", "utf-8");
467
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
468
+ $schema: "https://tsonic.org/schema/v1.json",
469
+ rootNamespace: "Acme.App",
470
+ entryPoint: "src/App.ts",
471
+ sourceRoot: "src",
472
+ references: {
473
+ libraries: [
474
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
475
+ ],
476
+ },
477
+ outputDirectory: "generated",
478
+ outputName: "Acme.App",
479
+ output: {
480
+ type: "library",
481
+ targetFrameworks: ["net10.0"],
482
+ nativeAot: false,
483
+ generateDocumentation: false,
484
+ includeSymbols: false,
485
+ packable: false,
486
+ },
487
+ }, null, 2) + "\n", "utf-8");
488
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
489
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
490
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
491
+ writeFileSync(join(dir, "packages", "core", "src", "entities", "user.ts"), [
492
+ `export class User {`,
493
+ ` name: string = "";`,
494
+ ` active: boolean = false;`,
495
+ `}`,
496
+ ``,
497
+ ].join("\n"), "utf-8");
498
+ writeFileSync(join(dir, "packages", "core", "src", "db", "store.ts"), [
499
+ `import { asinterface } from "@tsonic/core/lang.js";`,
500
+ `import type { ExtensionMethods as Linq } from "@tsonic/dotnet/System.Linq.js";`,
501
+ `import { List } from "@tsonic/dotnet/System.Collections.Generic.js";`,
502
+ `import type { User } from "../entities/user.ts";`,
503
+ ``,
504
+ `type Query<T> = Linq<List<T>>;`,
505
+ ``,
506
+ `export class UserStore extends List<User> {`,
507
+ ` get ActiveUsers(): Query<User> {`,
508
+ ` return asinterface<Query<User>>(this);`,
509
+ ` }`,
510
+ `}`,
511
+ ``,
512
+ `export function createSeed(): List<User> {`,
513
+ ` return new List<User>();`,
514
+ `}`,
515
+ ``,
516
+ ].join("\n"), "utf-8");
517
+ writeFileSync(join(dir, "packages", "core", "src", "index.ts"), [
518
+ `export { UserStore, createSeed } from "./db/store.ts";`,
519
+ `export type { User } from "./entities/user.ts";`,
520
+ ``,
521
+ ].join("\n"), "utf-8");
522
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
523
+ `import type { int } from "@tsonic/core/types.js";`,
524
+ `import { UserStore, createSeed } from "@acme/core/Acme.Core.db.js";`,
525
+ ``,
526
+ `export function project(store: UserStore): int {`,
527
+ ` const seeded = createSeed();`,
528
+ ` store.AddRange(seeded);`,
529
+ ` const active = store.ActiveUsers.Where((user) => user.active).ToArray();`,
530
+ ` return store.Count + active.Length;`,
531
+ `}`,
532
+ ``,
533
+ ].join("\n"), "utf-8");
534
+ runProjectBuild(dir, wsConfigPath, "core");
535
+ linkDir(join(dir, "packages", "core"), join(dir, "node_modules/@acme/core"));
536
+ const bindingsRoot = join(dir, "packages", "core", "dist", "tsonic", "bindings");
537
+ const internal = readFileSync(join(bindingsRoot, "Acme.Core.db", "internal", "index.d.ts"), "utf-8");
538
+ const facade = readFileSync(join(bindingsRoot, "Acme.Core.db.d.ts"), "utf-8");
539
+ const rootFacade = readFileSync(join(bindingsRoot, "Acme.Core.d.ts"), "utf-8");
540
+ expect(internal).to.include("import type { List } from '@tsonic/dotnet/System.Collections.Generic.js';");
541
+ expect(internal).to.include("import type { User } from '../../Acme.Core.entities/internal/index.js';");
542
+ expect(internal).to.match(/interface UserStore\$instance extends List<User>/);
543
+ expect(internal).to.match(/readonly ActiveUsers: __TsonicExt_Linq<List<User>>;/);
544
+ expect(facade).to.include("import type { List } from '@tsonic/dotnet/System.Collections.Generic.js';");
545
+ expect(facade).to.include("import type { User } from './Acme.Core.entities.js';");
546
+ expect(facade).to.match(/export declare function createSeed\(\): List<User>;/);
547
+ expect(rootFacade).to.include("import type { List } from '@tsonic/dotnet/System.Collections.Generic.js';");
548
+ expect(rootFacade).to.include("import type { User } from './Acme.Core.entities.js';");
549
+ expect(rootFacade).to.match(/export declare function createSeed\(\): List<User>;/);
550
+ runProjectBuild(dir, wsConfigPath, "app");
551
+ }
552
+ finally {
553
+ rmSync(dir, { recursive: true, force: true });
554
+ }
555
+ });
556
+ it("preserves EF/LINQ contextual typing across source-package bindings", () => {
557
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-ef-context-"));
558
+ try {
559
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
560
+ mkdirSync(join(dir, "packages", "core", "src", "db"), {
561
+ recursive: true,
562
+ });
563
+ mkdirSync(join(dir, "packages", "core", "src", "entities"), {
564
+ recursive: true,
565
+ });
566
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
567
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
568
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
569
+ name: "test",
570
+ private: true,
571
+ type: "module",
572
+ workspaces: ["packages/*"],
573
+ }, null, 2) + "\n", "utf-8");
574
+ writeFileSync(wsConfigPath, JSON.stringify({
575
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
576
+ dotnetVersion: "net10.0",
577
+ dotnet: {
578
+ packageReferences: [
579
+ {
580
+ id: "Microsoft.EntityFrameworkCore",
581
+ version: "10.0.1",
582
+ types: "@tsonic/efcore",
583
+ },
584
+ ],
585
+ libraries: [],
586
+ frameworkReferences: [],
587
+ },
588
+ }, null, 2) + "\n", "utf-8");
589
+ writeFileSync(join(dir, "packages", "core", "package.json"), JSON.stringify({
590
+ name: "@acme/core",
591
+ private: true,
592
+ type: "module",
593
+ exports: {
594
+ "./package.json": "./package.json",
595
+ "./*.js": {
596
+ types: "./dist/tsonic/bindings/*.d.ts",
597
+ default: "./dist/tsonic/bindings/*.js",
598
+ },
599
+ },
600
+ }, null, 2) + "\n", "utf-8");
601
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
602
+ name: "@acme/app",
603
+ private: true,
604
+ type: "module",
605
+ dependencies: {
606
+ "@acme/core": "workspace:*",
607
+ },
608
+ }, null, 2) + "\n", "utf-8");
609
+ writeFileSync(join(dir, "packages", "core", "tsonic.json"), JSON.stringify({
610
+ $schema: "https://tsonic.org/schema/v1.json",
611
+ rootNamespace: "Acme.Core",
612
+ entryPoint: "src/index.ts",
613
+ sourceRoot: "src",
614
+ outputDirectory: "generated",
615
+ outputName: "Acme.Core",
616
+ output: {
617
+ type: "library",
618
+ targetFrameworks: ["net10.0"],
619
+ nativeAot: false,
620
+ generateDocumentation: false,
621
+ includeSymbols: false,
622
+ packable: false,
623
+ },
624
+ }, null, 2) + "\n", "utf-8");
625
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
626
+ $schema: "https://tsonic.org/schema/v1.json",
627
+ rootNamespace: "Acme.App",
628
+ entryPoint: "src/App.ts",
629
+ sourceRoot: "src",
630
+ references: {
631
+ libraries: [
632
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
633
+ ],
634
+ },
635
+ outputDirectory: "generated",
636
+ outputName: "Acme.App",
637
+ output: {
638
+ type: "library",
639
+ targetFrameworks: ["net10.0"],
640
+ nativeAot: false,
641
+ generateDocumentation: false,
642
+ includeSymbols: false,
643
+ packable: false,
644
+ },
645
+ }, null, 2) + "\n", "utf-8");
646
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
647
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
648
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
649
+ linkDir(join(repoRoot, "node_modules/@tsonic/efcore"), join(dir, "node_modules/@tsonic/efcore"));
650
+ writeFileSync(join(dir, "packages", "core", "src", "entities", "user.ts"), [
651
+ `export class User {`,
652
+ ` email: string = "";`,
653
+ ` active: boolean = false;`,
654
+ `}`,
655
+ ``,
656
+ ].join("\n"), "utf-8");
657
+ writeFileSync(join(dir, "packages", "core", "src", "db", "context.ts"), [
658
+ `import type { ExtensionMethods as Linq, IQueryable } from "@tsonic/dotnet/System.Linq.js";`,
659
+ `import type { ExtensionMethods as Ef } from "@tsonic/efcore/Microsoft.EntityFrameworkCore.js";`,
660
+ `import type { User } from "../entities/user.ts";`,
661
+ ``,
662
+ `type Query<T> = Ef<Linq<IQueryable<T>>>;`,
663
+ ``,
664
+ `export class UserContext {`,
665
+ ` get Users(): Query<User> {`,
666
+ ` throw new Error("unused");`,
667
+ ` }`,
668
+ `}`,
669
+ ``,
670
+ ].join("\n"), "utf-8");
671
+ writeFileSync(join(dir, "packages", "core", "src", "index.ts"), [
672
+ `export { UserContext } from "./db/context.ts";`,
673
+ `export type { User } from "./entities/user.ts";`,
674
+ ``,
675
+ ].join("\n"), "utf-8");
676
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
677
+ `import { UserContext } from "@acme/core/Acme.Core.db.js";`,
678
+ ``,
679
+ `export async function findActive(db: UserContext): Promise<boolean> {`,
680
+ ` const user = await db.Users`,
681
+ ` .Where((u) => u.active)`,
682
+ ` .Where((u) => u.email !== "")`,
683
+ ` .FirstOrDefaultAsync();`,
684
+ ` return user === undefined ? false : user.active;`,
685
+ `}`,
686
+ ``,
687
+ ].join("\n"), "utf-8");
688
+ runProjectBuild(dir, wsConfigPath, "core");
689
+ linkDir(join(dir, "packages", "core"), join(dir, "node_modules/@acme/core"));
690
+ const bindingsRoot = join(dir, "packages", "core", "dist", "tsonic", "bindings");
691
+ const internal = readFileSync(join(bindingsRoot, "Acme.Core.db", "internal", "index.d.ts"), "utf-8");
692
+ expect(internal).to.match(/readonly Users: __TsonicExt_Ef<__TsonicExt_Linq<IQueryable<User>>>;/);
693
+ runProjectBuild(dir, wsConfigPath, "app");
694
+ }
695
+ finally {
696
+ rmSync(dir, { recursive: true, force: true });
697
+ }
698
+ });
699
+ it("preserves contextual object typing for imported generic result helpers across source-package bindings", () => {
700
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-contextual-result-"));
701
+ try {
702
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
703
+ mkdirSync(join(dir, "packages", "core", "src", "types"), {
704
+ recursive: true,
705
+ });
706
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
707
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
708
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
709
+ name: "test",
710
+ private: true,
711
+ type: "module",
712
+ workspaces: ["packages/*"],
713
+ }, null, 2) + "\n", "utf-8");
714
+ writeFileSync(wsConfigPath, JSON.stringify({
715
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
716
+ dotnetVersion: "net10.0",
717
+ }, null, 2) + "\n", "utf-8");
718
+ writeFileSync(join(dir, "packages", "core", "package.json"), JSON.stringify({
719
+ name: "@acme/core",
720
+ private: true,
721
+ type: "module",
722
+ exports: {
723
+ "./package.json": "./package.json",
724
+ "./*.js": {
725
+ types: "./dist/tsonic/bindings/*.d.ts",
726
+ default: "./dist/tsonic/bindings/*.js",
727
+ },
728
+ },
729
+ }, null, 2) + "\n", "utf-8");
730
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
731
+ name: "@acme/app",
732
+ private: true,
733
+ type: "module",
734
+ dependencies: {
735
+ "@acme/core": "workspace:*",
736
+ },
737
+ }, null, 2) + "\n", "utf-8");
738
+ writeFileSync(join(dir, "packages", "core", "tsonic.json"), JSON.stringify({
739
+ $schema: "https://tsonic.org/schema/v1.json",
740
+ rootNamespace: "Acme.Core",
741
+ entryPoint: "src/index.ts",
742
+ sourceRoot: "src",
743
+ outputDirectory: "generated",
744
+ outputName: "Acme.Core",
745
+ output: {
746
+ type: "library",
747
+ targetFrameworks: ["net10.0"],
748
+ nativeAot: false,
749
+ generateDocumentation: false,
750
+ includeSymbols: false,
751
+ packable: false,
752
+ },
753
+ }, null, 2) + "\n", "utf-8");
754
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
755
+ $schema: "https://tsonic.org/schema/v1.json",
756
+ rootNamespace: "Acme.App",
757
+ entryPoint: "src/App.ts",
758
+ sourceRoot: "src",
759
+ references: {
760
+ libraries: [
761
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
762
+ ],
763
+ },
764
+ outputDirectory: "generated",
765
+ outputName: "Acme.App",
766
+ output: {
767
+ type: "library",
768
+ targetFrameworks: ["net10.0"],
769
+ nativeAot: false,
770
+ generateDocumentation: false,
771
+ includeSymbols: false,
772
+ packable: false,
773
+ },
774
+ }, null, 2) + "\n", "utf-8");
775
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
776
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
777
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
778
+ writeFileSync(join(dir, "packages", "core", "src", "types", "result.ts"), [
779
+ `export type Ok<T> = { success: true; data: T };`,
780
+ `export type Err<E> = { success: false; error: E };`,
781
+ `export type Result<T, E> = Ok<T> | Err<E>;`,
782
+ ``,
783
+ `export function ok<T>(data: T): Ok<T> {`,
784
+ ` return { success: true, data };`,
785
+ `}`,
786
+ ``,
787
+ ].join("\n"), "utf-8");
788
+ writeFileSync(join(dir, "packages", "core", "src", "index.ts"), [
789
+ `export { ok } from "./types/result.ts";`,
790
+ `export type { Result } from "./types/result.ts";`,
791
+ ``,
792
+ ].join("\n"), "utf-8");
793
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
794
+ `import type { Result } from "@acme/core/Acme.Core.js";`,
795
+ `import { ok } from "@acme/core/Acme.Core.js";`,
796
+ ``,
797
+ `interface Payload {`,
798
+ ` foundAnchor: boolean;`,
799
+ ` foundNewest: boolean;`,
800
+ ` foundOldest: boolean;`,
801
+ `}`,
802
+ ``,
803
+ `export async function run(anchor: string): Promise<Result<Payload, string>> {`,
804
+ ` const foundAnchor = anchor !== "newest" && anchor !== "oldest";`,
805
+ ` const foundNewest = anchor === "newest";`,
806
+ ` const foundOldest = anchor === "oldest";`,
807
+ ` return ok({ foundAnchor, foundNewest, foundOldest });`,
808
+ `}`,
809
+ ``,
810
+ ].join("\n"), "utf-8");
811
+ runProjectBuild(dir, wsConfigPath, "core");
812
+ linkDir(join(dir, "packages", "core"), join(dir, "node_modules/@acme/core"));
813
+ runProjectBuild(dir, wsConfigPath, "app");
814
+ const generated = readFileSync(join(dir, "packages", "app", "generated", "App.cs"), "utf-8");
815
+ expect(generated).to.include("new Payload");
816
+ expect(generated).to.not.include("__Anon");
817
+ }
818
+ finally {
819
+ rmSync(dir, { recursive: true, force: true });
820
+ }
821
+ });
822
+ it("preserves imported alias return types for exported source-package function values", () => {
823
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-imported-return-alias-"));
824
+ try {
825
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
826
+ mkdirSync(join(dir, "packages", "core", "src"), { recursive: true });
827
+ mkdirSync(join(dir, "packages", "messages", "src"), { recursive: true });
828
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
829
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
830
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
831
+ name: "test",
832
+ private: true,
833
+ type: "module",
834
+ workspaces: ["packages/*"],
835
+ }, null, 2) + "\n", "utf-8");
836
+ writeFileSync(wsConfigPath, JSON.stringify({
837
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
838
+ dotnetVersion: "net10.0",
839
+ }, null, 2) + "\n", "utf-8");
840
+ for (const [name, rootNamespace] of [
841
+ ["core", "Acme.Core"],
842
+ ["messages", "Acme.Messages"],
843
+ ["app", "Acme.App"],
844
+ ]) {
845
+ writeFileSync(join(dir, "packages", name, "package.json"), JSON.stringify({
846
+ name: `@acme/${name}`,
847
+ private: true,
848
+ type: "module",
849
+ dependencies: name === "messages"
850
+ ? { "@acme/core": "workspace:*" }
851
+ : name === "app"
852
+ ? {
853
+ "@acme/core": "workspace:*",
854
+ "@acme/messages": "workspace:*",
855
+ }
856
+ : undefined,
857
+ exports: name === "app"
858
+ ? undefined
859
+ : {
860
+ "./package.json": "./package.json",
861
+ "./*.js": {
862
+ types: "./dist/tsonic/bindings/*.d.ts",
863
+ default: "./dist/tsonic/bindings/*.js",
864
+ },
865
+ },
866
+ }, null, 2) + "\n", "utf-8");
867
+ writeFileSync(join(dir, "packages", name, "tsonic.json"), JSON.stringify({
868
+ $schema: "https://tsonic.org/schema/v1.json",
869
+ rootNamespace,
870
+ entryPoint: name === "app" ? "src/App.ts" : "src/index.ts",
871
+ sourceRoot: "src",
872
+ references: name === "messages"
873
+ ? {
874
+ libraries: ["../core/generated/bin/Release/net10.0/Acme.Core.dll"],
875
+ }
876
+ : name === "app"
877
+ ? {
878
+ libraries: [
879
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
880
+ "../messages/generated/bin/Release/net10.0/Acme.Messages.dll",
881
+ ],
882
+ }
883
+ : undefined,
884
+ outputDirectory: "generated",
885
+ outputName: rootNamespace,
886
+ output: {
887
+ type: "library",
888
+ targetFrameworks: ["net10.0"],
889
+ nativeAot: false,
890
+ generateDocumentation: false,
891
+ includeSymbols: false,
892
+ packable: false,
893
+ },
894
+ }, null, 2) + "\n", "utf-8");
895
+ }
896
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
897
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
898
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
899
+ writeFileSync(join(dir, "packages", "core", "src", "index.ts"), [
900
+ `export interface AuthenticatedUser {`,
901
+ ` id: string;`,
902
+ `}`,
903
+ ``,
904
+ `export type Ok<T> = { success: true; data: T };`,
905
+ `export type Err<E> = { success: false; error: E };`,
906
+ `export type Result<T, E = string> = Ok<T> | Err<E>;`,
907
+ ``,
908
+ `export function ok<T>(data: T): Ok<T> {`,
909
+ ` return { success: true, data };`,
910
+ `}`,
911
+ ``,
912
+ `export function err<E>(error: E): Err<E> {`,
913
+ ` return { success: false, error };`,
914
+ `}`,
915
+ ``,
916
+ ].join("\n"), "utf-8");
917
+ writeFileSync(join(dir, "packages", "messages", "src", "index.ts"), [
918
+ `import type { AuthenticatedUser, Result } from "@acme/core/Acme.Core.js";`,
919
+ `import { err, ok } from "@acme/core/Acme.Core.js";`,
920
+ ``,
921
+ `interface SendMessageInput {`,
922
+ ` content: string;`,
923
+ `}`,
924
+ ``,
925
+ `export const sendMessageDomain = async (`,
926
+ ` user: AuthenticatedUser,`,
927
+ ` params: SendMessageInput`,
928
+ `): Promise<Result<{ id: string }, string>> => {`,
929
+ ` if (params.content === "") return err("empty");`,
930
+ ` return ok({ id: user.id });`,
931
+ `};`,
932
+ ``,
933
+ ].join("\n"), "utf-8");
934
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
935
+ `import { sendMessageDomain } from "@acme/messages/Acme.Messages.js";`,
936
+ ``,
937
+ `export async function run(): Promise<string> {`,
938
+ ` const result = await sendMessageDomain({ id: "u1" }, { content: "hello" });`,
939
+ ` if (!result.success) return result.error;`,
940
+ ` return result.data.id;`,
941
+ `}`,
942
+ ``,
943
+ ].join("\n"), "utf-8");
944
+ runProjectBuild(dir, wsConfigPath, "core");
945
+ linkDir(join(dir, "packages", "core"), join(dir, "node_modules/@acme/core"));
946
+ runProjectBuild(dir, wsConfigPath, "messages");
947
+ linkDir(join(dir, "packages", "messages"), join(dir, "node_modules/@acme/messages"));
948
+ const facade = readFileSync(join(dir, "packages", "messages", "dist", "tsonic", "bindings", "Acme.Messages.d.ts"), "utf-8");
949
+ const internal = readFileSync(join(dir, "packages", "messages", "dist", "tsonic", "bindings", "Acme.Messages", "internal", "index.d.ts"), "utf-8");
950
+ expect(facade).to.include(`import type { AuthenticatedUser } from '@acme/core/Acme.Core.js';`);
951
+ expect(facade).to.include(`import type { Result } from '@acme/core/Acme.Core.js';`);
952
+ expect(facade).to.match(/export declare const sendMessageDomain: \(user: AuthenticatedUser, params: __Local_.*SendMessageInput\) => Promise<Result<__Anon_.*?, string>>;/);
953
+ expect(facade).to.not.match(/Ok__Alias_1|Err__Alias_1/);
954
+ expect(internal).to.include(`import type { AuthenticatedUser } from '@acme/core/Acme.Core.js';`);
955
+ expect(internal).to.include(`import type { Result } from '@acme/core/Acme.Core.js';`);
956
+ expect(internal).to.match(/static sendMessageDomain: \(user: AuthenticatedUser, params: __Local_.*SendMessageInput\) => Promise<Result<__Anon_.*?, string>>;/);
957
+ expect(internal).to.not.match(/Ok__Alias_1|Err__Alias_1/);
958
+ runProjectBuild(dir, wsConfigPath, "app");
959
+ }
960
+ finally {
961
+ rmSync(dir, { recursive: true, force: true });
962
+ }
963
+ });
964
+ it("preserves canonical type identity for cross-namespace source re-exports", () => {
965
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-canonical-reexport-"));
966
+ try {
967
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
968
+ mkdirSync(join(dir, "packages", "core", "src", "db"), {
969
+ recursive: true,
970
+ });
971
+ mkdirSync(join(dir, "packages", "core", "src", "entities"), {
972
+ recursive: true,
973
+ });
974
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
975
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
976
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
977
+ name: "test",
978
+ private: true,
979
+ type: "module",
980
+ workspaces: ["packages/*"],
981
+ }, null, 2) + "\n", "utf-8");
982
+ writeFileSync(wsConfigPath, JSON.stringify({
983
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
984
+ dotnetVersion: "net10.0",
985
+ dotnet: {
986
+ packageReferences: [
987
+ {
988
+ id: "Microsoft.EntityFrameworkCore",
989
+ version: "10.0.1",
990
+ types: "@tsonic/efcore",
991
+ },
992
+ ],
993
+ libraries: [],
994
+ frameworkReferences: [],
995
+ },
996
+ }, null, 2) + "\n", "utf-8");
997
+ writeFileSync(join(dir, "packages", "core", "package.json"), JSON.stringify({
998
+ name: "@acme/core",
999
+ private: true,
1000
+ type: "module",
1001
+ exports: {
1002
+ "./package.json": "./package.json",
1003
+ "./*.js": {
1004
+ types: "./dist/tsonic/bindings/*.d.ts",
1005
+ default: "./dist/tsonic/bindings/*.js",
1006
+ },
1007
+ },
1008
+ }, null, 2) + "\n", "utf-8");
1009
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1010
+ name: "@acme/app",
1011
+ private: true,
1012
+ type: "module",
1013
+ dependencies: {
1014
+ "@acme/core": "workspace:*",
1015
+ },
1016
+ }, null, 2) + "\n", "utf-8");
1017
+ writeFileSync(join(dir, "packages", "core", "tsonic.json"), JSON.stringify({
1018
+ $schema: "https://tsonic.org/schema/v1.json",
1019
+ rootNamespace: "Acme.Core",
1020
+ entryPoint: "src/index.ts",
1021
+ sourceRoot: "src",
1022
+ outputDirectory: "generated",
1023
+ outputName: "Acme.Core",
1024
+ output: {
1025
+ type: "library",
1026
+ targetFrameworks: ["net10.0"],
1027
+ nativeAot: false,
1028
+ generateDocumentation: false,
1029
+ includeSymbols: false,
1030
+ packable: false,
1031
+ },
1032
+ }, null, 2) + "\n", "utf-8");
1033
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1034
+ $schema: "https://tsonic.org/schema/v1.json",
1035
+ rootNamespace: "Acme.App",
1036
+ entryPoint: "src/App.ts",
1037
+ sourceRoot: "src",
1038
+ references: {
1039
+ libraries: [
1040
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
1041
+ ],
1042
+ },
1043
+ outputDirectory: "generated",
1044
+ outputName: "Acme.App",
1045
+ output: {
1046
+ type: "library",
1047
+ targetFrameworks: ["net10.0"],
1048
+ nativeAot: false,
1049
+ generateDocumentation: false,
1050
+ includeSymbols: false,
1051
+ packable: false,
1052
+ },
1053
+ }, null, 2) + "\n", "utf-8");
1054
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
1055
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
1056
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
1057
+ linkDir(join(repoRoot, "node_modules/@tsonic/efcore"), join(dir, "node_modules/@tsonic/efcore"));
1058
+ writeFileSync(join(dir, "packages", "core", "src", "entities", "user.ts"), [
1059
+ `export class User {`,
1060
+ ` email: string = "";`,
1061
+ ` active: boolean = false;`,
1062
+ `}`,
1063
+ ``,
1064
+ ].join("\n"), "utf-8");
1065
+ writeFileSync(join(dir, "packages", "core", "src", "db", "context.ts"), [
1066
+ `import type { ExtensionMethods as Linq, IQueryable } from "@tsonic/dotnet/System.Linq.js";`,
1067
+ `import type { ExtensionMethods as Ef } from "@tsonic/efcore/Microsoft.EntityFrameworkCore.js";`,
1068
+ `import type { User } from "../entities/user.ts";`,
1069
+ ``,
1070
+ `type Query<T> = Ef<Linq<IQueryable<T>>>;`,
1071
+ ``,
1072
+ `export class UserContext {`,
1073
+ ` get Users(): Query<User> {`,
1074
+ ` throw new Error("unused");`,
1075
+ ` }`,
1076
+ `}`,
1077
+ ``,
1078
+ ].join("\n"), "utf-8");
1079
+ writeFileSync(join(dir, "packages", "core", "src", "index.ts"), [
1080
+ `export { UserContext } from "./db/context.ts";`,
1081
+ `export { User } from "./entities/user.ts";`,
1082
+ ``,
1083
+ ].join("\n"), "utf-8");
1084
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
1085
+ `import { User, UserContext } from "@acme/core/Acme.Core.js";`,
1086
+ ``,
1087
+ `export async function findActive(db: UserContext): Promise<User | undefined> {`,
1088
+ ` const user = await db.Users`,
1089
+ ` .Where((u) => u.active)`,
1090
+ ` .Where((u) => u.email !== "")`,
1091
+ ` .FirstOrDefaultAsync();`,
1092
+ ` return user;`,
1093
+ `}`,
1094
+ ``,
1095
+ ].join("\n"), "utf-8");
1096
+ runProjectBuild(dir, wsConfigPath, "core");
1097
+ linkDir(join(dir, "packages", "core"), join(dir, "node_modules/@acme/core"));
1098
+ const bindingsRoot = join(dir, "packages", "core", "dist", "tsonic", "bindings");
1099
+ const rootInternal = readFileSync(join(bindingsRoot, "Acme.Core", "internal", "index.d.ts"), "utf-8");
1100
+ const rootFacade = readFileSync(join(bindingsRoot, "Acme.Core.d.ts"), "utf-8");
1101
+ const rootBindings = JSON.parse(readFileSync(join(bindingsRoot, "Acme.Core", "bindings.json"), "utf-8"));
1102
+ expect(rootInternal).to.not.include("export interface User$instance");
1103
+ expect(rootFacade).to.not.include("export { User } from './Acme.Core/internal/index.js';");
1104
+ expect(rootBindings.types.map((item) => item.clrName)).to.not.include("Acme.Core.User");
1105
+ runProjectBuild(dir, wsConfigPath, "app");
1106
+ }
1107
+ finally {
1108
+ rmSync(dir, { recursive: true, force: true });
1109
+ }
1110
+ });
1111
+ it("honors included module augmentations during project build", () => {
1112
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-augmentation-"));
1113
+ try {
1114
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
1115
+ mkdirSync(join(dir, "packages", "core", "src"), { recursive: true });
1116
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
1117
+ mkdirSync(join(dir, "types"), { recursive: true });
1118
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
1119
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
1120
+ name: "test",
1121
+ private: true,
1122
+ type: "module",
1123
+ workspaces: ["packages/*"],
1124
+ }, null, 2) + "\n", "utf-8");
1125
+ writeFileSync(wsConfigPath, JSON.stringify({
1126
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
1127
+ dotnetVersion: "net10.0",
1128
+ }, null, 2) + "\n", "utf-8");
1129
+ writeFileSync(join(dir, "packages", "core", "package.json"), JSON.stringify({
1130
+ name: "@acme/core",
1131
+ private: true,
1132
+ type: "module",
1133
+ exports: {
1134
+ "./package.json": "./package.json",
1135
+ "./*.js": {
1136
+ types: "./dist/tsonic/bindings/*.d.ts",
1137
+ default: "./dist/tsonic/bindings/*.js",
1138
+ },
1139
+ },
1140
+ }, null, 2) + "\n", "utf-8");
1141
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1142
+ name: "@acme/app",
1143
+ private: true,
1144
+ type: "module",
1145
+ dependencies: {
1146
+ "@acme/core": "workspace:*",
1147
+ },
1148
+ }, null, 2) + "\n", "utf-8");
1149
+ writeFileSync(join(dir, "packages", "core", "tsonic.json"), JSON.stringify({
1150
+ $schema: "https://tsonic.org/schema/v1.json",
1151
+ rootNamespace: "Acme.Core",
1152
+ entryPoint: "src/index.ts",
1153
+ sourceRoot: "src",
1154
+ outputDirectory: "generated",
1155
+ outputName: "Acme.Core",
1156
+ output: {
1157
+ type: "library",
1158
+ targetFrameworks: ["net10.0"],
1159
+ nativeAot: false,
1160
+ generateDocumentation: false,
1161
+ includeSymbols: false,
1162
+ packable: false,
1163
+ },
1164
+ }, null, 2) + "\n", "utf-8");
1165
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1166
+ $schema: "https://tsonic.org/schema/v1.json",
1167
+ rootNamespace: "Acme.App",
1168
+ entryPoint: "src/App.ts",
1169
+ sourceRoot: "src",
1170
+ references: {
1171
+ libraries: [
1172
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
1173
+ ],
1174
+ },
1175
+ outputDirectory: "generated",
1176
+ outputName: "Acme.App",
1177
+ output: {
1178
+ type: "library",
1179
+ targetFrameworks: ["net10.0"],
1180
+ nativeAot: false,
1181
+ generateDocumentation: false,
1182
+ includeSymbols: false,
1183
+ packable: false,
1184
+ },
1185
+ }, null, 2) + "\n", "utf-8");
1186
+ writeFileSync(join(dir, "packages", "app", "tsconfig.json"), JSON.stringify({
1187
+ include: ["src/**/*.ts", "../../types/**/*.d.ts"],
1188
+ }, null, 2) + "\n", "utf-8");
1189
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
1190
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
1191
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
1192
+ writeFileSync(join(dir, "packages", "core", "src", "index.ts"), [
1193
+ `export class Greeter {`,
1194
+ ` greet(name: string): string {`,
1195
+ ` return "hello " + name;`,
1196
+ ` }`,
1197
+ `}`,
1198
+ ``,
1199
+ ].join("\n"), "utf-8");
1200
+ writeFileSync(join(dir, "types", "acme-core-augment.d.ts"), [
1201
+ `declare module "@acme/core/Acme.Core.js" {`,
1202
+ ` export interface AugmentedSentinel {`,
1203
+ ` ok: true;`,
1204
+ ` }`,
1205
+ `}`,
1206
+ ``,
1207
+ ].join("\n"), "utf-8");
1208
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
1209
+ `import type { AugmentedSentinel } from "@acme/core/Acme.Core.js";`,
1210
+ ``,
1211
+ `export type VisibleAugmentation = AugmentedSentinel;`,
1212
+ ``,
1213
+ `export function ok(): void {}`,
1214
+ ``,
1215
+ ].join("\n"), "utf-8");
1216
+ runProjectBuild(dir, wsConfigPath, "core");
1217
+ linkDir(join(dir, "packages", "core"), join(dir, "node_modules/@acme/core"));
1218
+ runProjectBuild(dir, wsConfigPath, "app");
1219
+ }
1220
+ finally {
1221
+ rmSync(dir, { recursive: true, force: true });
1222
+ }
1223
+ });
1224
+ it("preserves non-exported local type closure without leaking raw local names across namespace facades", () => {
1225
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-private-local-closure-"));
1226
+ try {
1227
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
1228
+ mkdirSync(join(dir, "packages", "messages", "src", "domain"), {
1229
+ recursive: true,
1230
+ });
1231
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
1232
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
1233
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
1234
+ name: "test",
1235
+ private: true,
1236
+ type: "module",
1237
+ workspaces: ["packages/*"],
1238
+ }, null, 2) + "\n", "utf-8");
1239
+ writeFileSync(wsConfigPath, JSON.stringify({
1240
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
1241
+ dotnetVersion: "net10.0",
1242
+ dotnet: {
1243
+ packageReferences: [],
1244
+ libraries: [],
1245
+ frameworkReferences: [],
1246
+ },
1247
+ }, null, 2) + "\n", "utf-8");
1248
+ writeFileSync(join(dir, "packages", "messages", "package.json"), JSON.stringify({
1249
+ name: "@acme/messages",
1250
+ private: true,
1251
+ type: "module",
1252
+ exports: {
1253
+ "./package.json": "./package.json",
1254
+ "./*.js": {
1255
+ types: "./dist/tsonic/bindings/*.d.ts",
1256
+ default: "./dist/tsonic/bindings/*.js",
1257
+ },
1258
+ },
1259
+ }, null, 2) + "\n", "utf-8");
1260
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1261
+ name: "@acme/app",
1262
+ private: true,
1263
+ type: "module",
1264
+ dependencies: {
1265
+ "@acme/messages": "workspace:*",
1266
+ },
1267
+ }, null, 2) + "\n", "utf-8");
1268
+ writeFileSync(join(dir, "packages", "messages", "tsonic.json"), JSON.stringify({
1269
+ $schema: "https://tsonic.org/schema/v1.json",
1270
+ rootNamespace: "Acme.Messages",
1271
+ entryPoint: "src/index.ts",
1272
+ sourceRoot: "src",
1273
+ outputDirectory: "generated",
1274
+ outputName: "Acme.Messages",
1275
+ output: {
1276
+ type: "library",
1277
+ targetFrameworks: ["net10.0"],
1278
+ nativeAot: false,
1279
+ generateDocumentation: false,
1280
+ includeSymbols: false,
1281
+ packable: false,
1282
+ },
1283
+ }, null, 2) + "\n", "utf-8");
1284
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1285
+ $schema: "https://tsonic.org/schema/v1.json",
1286
+ rootNamespace: "Acme.App",
1287
+ entryPoint: "src/App.ts",
1288
+ sourceRoot: "src",
1289
+ references: {
1290
+ libraries: [
1291
+ "../messages/generated/bin/Release/net10.0/Acme.Messages.dll",
1292
+ ],
1293
+ },
1294
+ outputDirectory: "generated",
1295
+ outputName: "Acme.App",
1296
+ output: {
1297
+ type: "library",
1298
+ targetFrameworks: ["net10.0"],
1299
+ nativeAot: false,
1300
+ generateDocumentation: false,
1301
+ includeSymbols: false,
1302
+ packable: false,
1303
+ },
1304
+ }, null, 2) + "\n", "utf-8");
1305
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
1306
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
1307
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
1308
+ writeFileSync(join(dir, "packages", "messages", "src", "index.ts"), [
1309
+ `export { sendA } from "./domain/send-a.ts";`,
1310
+ `export { sendB } from "./domain/send-b.ts";`,
1311
+ ``,
1312
+ ].join("\n"), "utf-8");
1313
+ writeFileSync(join(dir, "packages", "messages", "src", "domain", "send-a.ts"), [
1314
+ `interface SendAInput {`,
1315
+ ` value: string;`,
1316
+ `}`,
1317
+ ``,
1318
+ `export const sendA = (input: SendAInput): SendAInput => input;`,
1319
+ ``,
1320
+ ].join("\n"), "utf-8");
1321
+ writeFileSync(join(dir, "packages", "messages", "src", "domain", "send-b.ts"), [
1322
+ `import type { int } from "@tsonic/core/types.js";`,
1323
+ ``,
1324
+ `interface SendBInput {`,
1325
+ ` count: int;`,
1326
+ `}`,
1327
+ ``,
1328
+ `export const sendB = (input: SendBInput): SendBInput => input;`,
1329
+ ``,
1330
+ ].join("\n"), "utf-8");
1331
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
1332
+ `import type { int } from "@tsonic/core/types.js";`,
1333
+ `import { sendA, sendB } from "@acme/messages/Acme.Messages.domain.js";`,
1334
+ ``,
1335
+ `export function run(): int {`,
1336
+ ` const left = sendA({ value: "ok" });`,
1337
+ ` const right = sendB({ count: 1 as int });`,
1338
+ ` const text: string = left.value;`,
1339
+ ` void text;`,
1340
+ ` return right.count;`,
1341
+ `}`,
1342
+ ``,
1343
+ ].join("\n"), "utf-8");
1344
+ runProjectBuild(dir, wsConfigPath, "messages");
1345
+ linkDir(join(dir, "packages", "messages"), join(dir, "node_modules/@acme/messages"));
1346
+ const facade = readFileSync(join(dir, "packages", "messages", "dist", "tsonic", "bindings", "Acme.Messages.domain.d.ts"), "utf-8");
1347
+ const internal = readFileSync(join(dir, "packages", "messages", "dist", "tsonic", "bindings", "Acme.Messages.domain", "internal", "index.d.ts"), "utf-8");
1348
+ const stripInternalMarkers = (text) => text
1349
+ .replace(/^\s*readonly __tsonic_type_[^\n]*\n/gm, "")
1350
+ .replace(/^\s*readonly "__tsonic_binding_alias_[^"]+"[^\n]*\n/gm, "");
1351
+ const sanitizedFacade = stripInternalMarkers(facade);
1352
+ const sanitizedInternal = stripInternalMarkers(internal);
1353
+ expect(sanitizedFacade).to.include("import type { __Local_");
1354
+ expect(sanitizedFacade).to.not.match(/\bSendAInput\b/);
1355
+ expect(sanitizedFacade).to.not.match(/\bSendBInput\b/);
1356
+ expect(sanitizedInternal).to.include("export interface __Local_");
1357
+ expect(sanitizedInternal).to.not.match(/\bSendAInput\b/);
1358
+ expect(sanitizedInternal).to.not.match(/\bSendBInput\b/);
1359
+ runProjectBuild(dir, wsConfigPath, "app");
1360
+ }
1361
+ finally {
1362
+ rmSync(dir, { recursive: true, force: true });
1363
+ }
1364
+ });
1365
+ it("keeps anonymous inline object parameters structural across source-package consumers", () => {
1366
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-anon-param-"));
1367
+ try {
1368
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
1369
+ mkdirSync(join(dir, "packages", "messages", "src", "domain"), {
1370
+ recursive: true,
1371
+ });
1372
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
1373
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
1374
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
1375
+ name: "test",
1376
+ private: true,
1377
+ type: "module",
1378
+ workspaces: ["packages/*"],
1379
+ }, null, 2) + "\n", "utf-8");
1380
+ writeFileSync(wsConfigPath, JSON.stringify({
1381
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
1382
+ dotnetVersion: "net10.0",
1383
+ }, null, 2) + "\n", "utf-8");
1384
+ writeFileSync(join(dir, "packages", "messages", "package.json"), JSON.stringify({
1385
+ name: "@acme/messages",
1386
+ private: true,
1387
+ type: "module",
1388
+ exports: {
1389
+ "./package.json": "./package.json",
1390
+ "./*.js": {
1391
+ types: "./dist/tsonic/bindings/*.d.ts",
1392
+ default: "./dist/tsonic/bindings/*.js",
1393
+ },
1394
+ },
1395
+ }, null, 2) + "\n", "utf-8");
1396
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1397
+ name: "app",
1398
+ private: true,
1399
+ type: "module",
1400
+ }, null, 2) + "\n", "utf-8");
1401
+ writeFileSync(join(dir, "packages", "messages", "tsonic.json"), JSON.stringify({
1402
+ $schema: "https://tsonic.org/schema/v1.json",
1403
+ rootNamespace: "Acme.Messages",
1404
+ entryPoint: "src/index.ts",
1405
+ sourceRoot: "src",
1406
+ outputDirectory: "generated",
1407
+ outputName: "Acme.Messages",
1408
+ output: {
1409
+ type: "library",
1410
+ targetFrameworks: ["net10.0"],
1411
+ nativeAot: false,
1412
+ generateDocumentation: false,
1413
+ includeSymbols: false,
1414
+ packable: false,
1415
+ },
1416
+ }, null, 2) + "\n", "utf-8");
1417
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1418
+ $schema: "https://tsonic.org/schema/v1.json",
1419
+ rootNamespace: "Acme.App",
1420
+ entryPoint: "src/App.ts",
1421
+ sourceRoot: "src",
1422
+ references: {
1423
+ libraries: [
1424
+ "../messages/generated/bin/Release/net10.0/Acme.Messages.dll",
1425
+ ],
1426
+ },
1427
+ outputDirectory: "generated",
1428
+ outputName: "Acme.App",
1429
+ output: {
1430
+ type: "library",
1431
+ targetFrameworks: ["net10.0"],
1432
+ nativeAot: false,
1433
+ generateDocumentation: false,
1434
+ includeSymbols: false,
1435
+ packable: false,
1436
+ },
1437
+ }, null, 2) + "\n", "utf-8");
1438
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
1439
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
1440
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
1441
+ writeFileSync(join(dir, "packages", "messages", "src", "index.ts"), [
1442
+ `export { createUserDomain } from "./domain/create-user-domain.ts";`,
1443
+ ``,
1444
+ ].join("\n"), "utf-8");
1445
+ writeFileSync(join(dir, "packages", "messages", "src", "domain", "create-user-domain.ts"), [
1446
+ `export const createUserDomain = (input: { email: string; fullName: string }): string => {`,
1447
+ ` return input.email + ":" + input.fullName;`,
1448
+ `};`,
1449
+ ``,
1450
+ ].join("\n"), "utf-8");
1451
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
1452
+ `import { createUserDomain } from "@acme/messages/Acme.Messages.js";`,
1453
+ ``,
1454
+ `export function run(): string {`,
1455
+ ` return createUserDomain({ email: "a@example.com", fullName: "Alice" });`,
1456
+ `}`,
1457
+ ``,
1458
+ ].join("\n"), "utf-8");
1459
+ runProjectBuild(dir, wsConfigPath, "messages");
1460
+ linkDir(join(dir, "packages", "messages"), join(dir, "node_modules/@acme/messages"));
1461
+ const internal = readFileSync(join(dir, "packages", "messages", "dist", "tsonic", "bindings", "Acme.Messages", "internal", "index.d.ts"), "utf-8");
1462
+ expect(internal).to.match(/export interface __Anon_/);
1463
+ expect(internal).to.not.match(/new\(...args: unknown\[\]\): __Anon_/);
1464
+ runProjectBuild(dir, wsConfigPath, "app");
1465
+ }
1466
+ finally {
1467
+ rmSync(dir, { recursive: true, force: true });
1468
+ }
1469
+ });
1470
+ it("keeps anonymous inline object parameters structural when passed via local variables", () => {
1471
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-anon-local-param-"));
1472
+ try {
1473
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
1474
+ mkdirSync(join(dir, "packages", "messages", "src", "domain"), {
1475
+ recursive: true,
1476
+ });
1477
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
1478
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
1479
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
1480
+ name: "test",
1481
+ private: true,
1482
+ type: "module",
1483
+ workspaces: ["packages/*"],
1484
+ }, null, 2) + "\n", "utf-8");
1485
+ writeFileSync(wsConfigPath, JSON.stringify({
1486
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
1487
+ dotnetVersion: "net10.0",
1488
+ }, null, 2) + "\n", "utf-8");
1489
+ writeFileSync(join(dir, "packages", "messages", "package.json"), JSON.stringify({
1490
+ name: "@acme/messages",
1491
+ private: true,
1492
+ type: "module",
1493
+ exports: {
1494
+ "./package.json": "./package.json",
1495
+ "./*.js": {
1496
+ types: "./dist/tsonic/bindings/*.d.ts",
1497
+ default: "./dist/tsonic/bindings/*.js",
1498
+ },
1499
+ },
1500
+ }, null, 2) + "\n", "utf-8");
1501
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1502
+ name: "app",
1503
+ private: true,
1504
+ type: "module",
1505
+ }, null, 2) + "\n", "utf-8");
1506
+ writeFileSync(join(dir, "packages", "messages", "tsonic.json"), JSON.stringify({
1507
+ $schema: "https://tsonic.org/schema/v1.json",
1508
+ rootNamespace: "Acme.Messages",
1509
+ entryPoint: "src/index.ts",
1510
+ sourceRoot: "src",
1511
+ outputDirectory: "generated",
1512
+ outputName: "Acme.Messages",
1513
+ output: {
1514
+ type: "library",
1515
+ targetFrameworks: ["net10.0"],
1516
+ nativeAot: false,
1517
+ generateDocumentation: false,
1518
+ includeSymbols: false,
1519
+ packable: false,
1520
+ },
1521
+ }, null, 2) + "\n", "utf-8");
1522
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1523
+ $schema: "https://tsonic.org/schema/v1.json",
1524
+ rootNamespace: "Acme.App",
1525
+ entryPoint: "src/App.ts",
1526
+ sourceRoot: "src",
1527
+ references: {
1528
+ libraries: [
1529
+ "../messages/generated/bin/Release/net10.0/Acme.Messages.dll",
1530
+ ],
1531
+ },
1532
+ outputDirectory: "generated",
1533
+ outputName: "Acme.App",
1534
+ output: {
1535
+ type: "library",
1536
+ targetFrameworks: ["net10.0"],
1537
+ nativeAot: false,
1538
+ generateDocumentation: false,
1539
+ includeSymbols: false,
1540
+ packable: false,
1541
+ },
1542
+ }, null, 2) + "\n", "utf-8");
1543
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
1544
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
1545
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
1546
+ writeFileSync(join(dir, "packages", "messages", "src", "index.ts"), [
1547
+ `export { createUserDomain } from "./domain/create-user-domain.ts";`,
1548
+ ``,
1549
+ ].join("\n"), "utf-8");
1550
+ writeFileSync(join(dir, "packages", "messages", "src", "domain", "create-user-domain.ts"), [
1551
+ `export const createUserDomain = (input: { email: string; fullName: string }): string => {`,
1552
+ ` return input.email + ":" + input.fullName;`,
1553
+ `};`,
1554
+ ``,
1555
+ ].join("\n"), "utf-8");
1556
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
1557
+ `import { createUserDomain } from "@acme/messages/Acme.Messages.js";`,
1558
+ ``,
1559
+ `export function run(): string {`,
1560
+ ` const input = { email: "a@example.com", fullName: "Alice" };`,
1561
+ ` return createUserDomain(input);`,
1562
+ `}`,
1563
+ ``,
1564
+ ].join("\n"), "utf-8");
1565
+ runProjectBuild(dir, wsConfigPath, "messages");
1566
+ linkDir(join(dir, "packages", "messages"), join(dir, "node_modules/@acme/messages"));
1567
+ runProjectBuild(dir, wsConfigPath, "app");
1568
+ }
1569
+ finally {
1570
+ rmSync(dir, { recursive: true, force: true });
1571
+ }
1572
+ });
1573
+ it("keeps anonymous inline object array parameters structural through local ToArray() values", () => {
1574
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-anon-array-local-"));
1575
+ try {
1576
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
1577
+ mkdirSync(join(dir, "packages", "messages", "src", "domain"), {
1578
+ recursive: true,
1579
+ });
1580
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
1581
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
1582
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
1583
+ name: "test",
1584
+ private: true,
1585
+ type: "module",
1586
+ workspaces: ["packages/*"],
1587
+ }, null, 2) + "\n", "utf-8");
1588
+ writeFileSync(wsConfigPath, JSON.stringify({
1589
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
1590
+ dotnetVersion: "net10.0",
1591
+ }, null, 2) + "\n", "utf-8");
1592
+ writeFileSync(join(dir, "packages", "messages", "package.json"), JSON.stringify({
1593
+ name: "@acme/messages",
1594
+ private: true,
1595
+ type: "module",
1596
+ exports: {
1597
+ "./package.json": "./package.json",
1598
+ "./*.js": {
1599
+ types: "./dist/tsonic/bindings/*.d.ts",
1600
+ default: "./dist/tsonic/bindings/*.js",
1601
+ },
1602
+ },
1603
+ }, null, 2) + "\n", "utf-8");
1604
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1605
+ name: "app",
1606
+ private: true,
1607
+ type: "module",
1608
+ }, null, 2) + "\n", "utf-8");
1609
+ writeFileSync(join(dir, "packages", "messages", "tsonic.json"), JSON.stringify({
1610
+ $schema: "https://tsonic.org/schema/v1.json",
1611
+ rootNamespace: "Acme.Messages",
1612
+ entryPoint: "src/index.ts",
1613
+ sourceRoot: "src",
1614
+ outputDirectory: "generated",
1615
+ outputName: "Acme.Messages",
1616
+ output: {
1617
+ type: "library",
1618
+ targetFrameworks: ["net10.0"],
1619
+ nativeAot: false,
1620
+ generateDocumentation: false,
1621
+ includeSymbols: false,
1622
+ packable: false,
1623
+ },
1624
+ }, null, 2) + "\n", "utf-8");
1625
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1626
+ $schema: "https://tsonic.org/schema/v1.json",
1627
+ rootNamespace: "Acme.App",
1628
+ entryPoint: "src/App.ts",
1629
+ sourceRoot: "src",
1630
+ references: {
1631
+ libraries: [
1632
+ "../messages/generated/bin/Release/net10.0/Acme.Messages.dll",
1633
+ ],
1634
+ },
1635
+ outputDirectory: "generated",
1636
+ outputName: "Acme.App",
1637
+ output: {
1638
+ type: "library",
1639
+ targetFrameworks: ["net10.0"],
1640
+ nativeAot: false,
1641
+ generateDocumentation: false,
1642
+ includeSymbols: false,
1643
+ packable: false,
1644
+ },
1645
+ }, null, 2) + "\n", "utf-8");
1646
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
1647
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
1648
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
1649
+ writeFileSync(join(dir, "packages", "messages", "src", "index.ts"), [
1650
+ `export { createDraftsDomain } from "./domain/create-drafts-domain.ts";`,
1651
+ ``,
1652
+ ].join("\n"), "utf-8");
1653
+ writeFileSync(join(dir, "packages", "messages", "src", "domain", "create-drafts-domain.ts"), [
1654
+ `export const createDraftsDomain = (drafts: { type: string; to: string; topic?: string; content: string }[]): string => {`,
1655
+ ` return drafts[0]?.content ?? "";`,
1656
+ `};`,
1657
+ ``,
1658
+ ].join("\n"), "utf-8");
1659
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
1660
+ `import { List } from "@tsonic/dotnet/System.Collections.Generic.js";`,
1661
+ `import { createDraftsDomain } from "@acme/messages/Acme.Messages.js";`,
1662
+ ``,
1663
+ `export function run(): string {`,
1664
+ ` const inputs = new List<{ type: string; to: string; topic?: string; content: string }>();`,
1665
+ ` inputs.Add({ type: "stream", to: "general", topic: "hello", content: "world" });`,
1666
+ ` const drafts = inputs.ToArray();`,
1667
+ ` return createDraftsDomain(drafts);`,
1668
+ `}`,
1669
+ ``,
1670
+ ].join("\n"), "utf-8");
1671
+ runProjectBuild(dir, wsConfigPath, "messages");
1672
+ linkDir(join(dir, "packages", "messages"), join(dir, "node_modules/@acme/messages"));
1673
+ runProjectBuild(dir, wsConfigPath, "app");
1674
+ }
1675
+ finally {
1676
+ rmSync(dir, { recursive: true, force: true });
1677
+ }
1678
+ });
1679
+ it("keeps nested anonymous record value types structural across source-package consumers", () => {
1680
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-anon-record-"));
1681
+ try {
1682
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
1683
+ mkdirSync(join(dir, "packages", "users", "src", "domain"), {
1684
+ recursive: true,
1685
+ });
1686
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
1687
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
1688
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
1689
+ name: "test",
1690
+ private: true,
1691
+ type: "module",
1692
+ workspaces: ["packages/*"],
1693
+ }, null, 2) + "\n", "utf-8");
1694
+ writeFileSync(wsConfigPath, JSON.stringify({
1695
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
1696
+ dotnetVersion: "net10.0",
1697
+ }, null, 2) + "\n", "utf-8");
1698
+ writeFileSync(join(dir, "packages", "users", "package.json"), JSON.stringify({
1699
+ name: "@acme/users",
1700
+ private: true,
1701
+ type: "module",
1702
+ exports: {
1703
+ "./package.json": "./package.json",
1704
+ "./*.js": {
1705
+ types: "./dist/tsonic/bindings/*.d.ts",
1706
+ default: "./dist/tsonic/bindings/*.js",
1707
+ },
1708
+ },
1709
+ }, null, 2) + "\n", "utf-8");
1710
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1711
+ name: "app",
1712
+ private: true,
1713
+ type: "module",
1714
+ }, null, 2) + "\n", "utf-8");
1715
+ writeFileSync(join(dir, "packages", "users", "tsonic.json"), JSON.stringify({
1716
+ $schema: "https://tsonic.org/schema/v1.json",
1717
+ rootNamespace: "Acme.Users",
1718
+ entryPoint: "src/index.ts",
1719
+ sourceRoot: "src",
1720
+ outputDirectory: "generated",
1721
+ outputName: "Acme.Users",
1722
+ output: {
1723
+ type: "library",
1724
+ targetFrameworks: ["net10.0"],
1725
+ nativeAot: false,
1726
+ generateDocumentation: false,
1727
+ includeSymbols: false,
1728
+ packable: false,
1729
+ },
1730
+ }, null, 2) + "\n", "utf-8");
1731
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1732
+ $schema: "https://tsonic.org/schema/v1.json",
1733
+ rootNamespace: "Acme.App",
1734
+ entryPoint: "src/App.ts",
1735
+ sourceRoot: "src",
1736
+ references: {
1737
+ libraries: ["../users/generated/bin/Release/net10.0/Acme.Users.dll"],
1738
+ },
1739
+ outputDirectory: "generated",
1740
+ outputName: "Acme.App",
1741
+ output: {
1742
+ type: "library",
1743
+ targetFrameworks: ["net10.0"],
1744
+ nativeAot: false,
1745
+ generateDocumentation: false,
1746
+ includeSymbols: false,
1747
+ packable: false,
1748
+ },
1749
+ }, null, 2) + "\n", "utf-8");
1750
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
1751
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
1752
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
1753
+ writeFileSync(join(dir, "packages", "users", "src", "index.ts"), [
1754
+ `export { updateProfileDataDomain } from "./domain/update-profile-data-domain.ts";`,
1755
+ ``,
1756
+ ].join("\n"), "utf-8");
1757
+ writeFileSync(join(dir, "packages", "users", "src", "domain", "update-profile-data-domain.ts"), [
1758
+ `export const updateProfileDataDomain = (profileData: Record<string, { value: string }>): string => {`,
1759
+ ` const item = profileData["name"];`,
1760
+ ` return item === undefined ? "" : item.value;`,
1761
+ `};`,
1762
+ ``,
1763
+ ].join("\n"), "utf-8");
1764
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
1765
+ `import { updateProfileDataDomain } from "@acme/users/Acme.Users.js";`,
1766
+ ``,
1767
+ `export function run(): string {`,
1768
+ ` return updateProfileDataDomain({ name: { value: "Alice" } });`,
1769
+ `}`,
1770
+ ``,
1771
+ ].join("\n"), "utf-8");
1772
+ runProjectBuild(dir, wsConfigPath, "users");
1773
+ linkDir(join(dir, "packages", "users"), join(dir, "node_modules/@acme/users"));
1774
+ const internal = readFileSync(join(dir, "packages", "users", "dist", "tsonic", "bindings", "Acme.Users", "internal", "index.d.ts"), "utf-8");
1775
+ expect(internal).to.not.match(/readonly __tsonic_type_[^\n]*__Anon_/);
1776
+ expect(internal).to.not.match(/new\(...args: unknown\[\]\): __Anon_/);
1777
+ runProjectBuild(dir, wsConfigPath, "app");
1778
+ }
1779
+ finally {
1780
+ rmSync(dir, { recursive: true, force: true });
1781
+ }
1782
+ });
1783
+ it("keeps same-named local helper types from sibling namespaces unambiguous for consumers", () => {
1784
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-sibling-local-type-"));
1785
+ try {
1786
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
1787
+ mkdirSync(join(dir, "packages", "channels", "src", "domain"), {
1788
+ recursive: true,
1789
+ });
1790
+ mkdirSync(join(dir, "packages", "channels", "src", "repo"), {
1791
+ recursive: true,
1792
+ });
1793
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
1794
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
1795
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
1796
+ name: "test",
1797
+ private: true,
1798
+ type: "module",
1799
+ workspaces: ["packages/*"],
1800
+ }, null, 2) + "\n", "utf-8");
1801
+ writeFileSync(wsConfigPath, JSON.stringify({
1802
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
1803
+ dotnetVersion: "net10.0",
1804
+ }, null, 2) + "\n", "utf-8");
1805
+ writeFileSync(join(dir, "packages", "channels", "package.json"), JSON.stringify({
1806
+ name: "@acme/channels",
1807
+ private: true,
1808
+ type: "module",
1809
+ exports: {
1810
+ "./package.json": "./package.json",
1811
+ "./*.js": {
1812
+ types: "./dist/tsonic/bindings/*.d.ts",
1813
+ default: "./dist/tsonic/bindings/*.js",
1814
+ },
1815
+ },
1816
+ }, null, 2) + "\n", "utf-8");
1817
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1818
+ name: "app",
1819
+ private: true,
1820
+ type: "module",
1821
+ }, null, 2) + "\n", "utf-8");
1822
+ writeFileSync(join(dir, "packages", "channels", "tsonic.json"), JSON.stringify({
1823
+ $schema: "https://tsonic.org/schema/v1.json",
1824
+ rootNamespace: "Acme.Channels",
1825
+ entryPoint: "src/index.ts",
1826
+ sourceRoot: "src",
1827
+ outputDirectory: "generated",
1828
+ outputName: "Acme.Channels",
1829
+ output: {
1830
+ type: "library",
1831
+ targetFrameworks: ["net10.0"],
1832
+ nativeAot: false,
1833
+ generateDocumentation: false,
1834
+ includeSymbols: false,
1835
+ packable: false,
1836
+ },
1837
+ }, null, 2) + "\n", "utf-8");
1838
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1839
+ $schema: "https://tsonic.org/schema/v1.json",
1840
+ rootNamespace: "Acme.App",
1841
+ entryPoint: "src/App.ts",
1842
+ sourceRoot: "src",
1843
+ references: {
1844
+ libraries: [
1845
+ "../channels/generated/bin/Release/net10.0/Acme.Channels.dll",
1846
+ ],
1847
+ },
1848
+ outputDirectory: "generated",
1849
+ outputName: "Acme.App",
1850
+ output: {
1851
+ type: "library",
1852
+ targetFrameworks: ["net10.0"],
1853
+ nativeAot: false,
1854
+ generateDocumentation: false,
1855
+ includeSymbols: false,
1856
+ packable: false,
1857
+ },
1858
+ }, null, 2) + "\n", "utf-8");
1859
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
1860
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
1861
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
1862
+ writeFileSync(join(dir, "packages", "channels", "src", "entities.ts"), [
1863
+ "export class ChannelFolder {",
1864
+ " Id: string = \"\";",
1865
+ "}",
1866
+ "",
1867
+ "export class ChannelFolderItem {",
1868
+ " ChannelId: string = \"\";",
1869
+ "}",
1870
+ "",
1871
+ ].join("\n"), "utf-8");
1872
+ writeFileSync(join(dir, "packages", "channels", "src", "repo", "get-channel-folders.ts"), [
1873
+ 'import { ChannelFolder, ChannelFolderItem } from "../entities.ts";',
1874
+ "",
1875
+ "interface ChannelFolderWithItems {",
1876
+ " folder: ChannelFolder;",
1877
+ " items: ChannelFolderItem[];",
1878
+ "}",
1879
+ "",
1880
+ "export const getChannelFolders = (): ChannelFolderWithItems[] => {",
1881
+ " const folder = new ChannelFolder();",
1882
+ ' folder.Id = "folder-1";',
1883
+ " const item = new ChannelFolderItem();",
1884
+ ' item.ChannelId = "channel-1";',
1885
+ " return [{ folder, items: [item] }];",
1886
+ "};",
1887
+ "",
1888
+ ].join("\n"), "utf-8");
1889
+ writeFileSync(join(dir, "packages", "channels", "src", "domain", "get-channel-folders-domain.ts"), [
1890
+ 'import { ChannelFolder, ChannelFolderItem } from "../entities.ts";',
1891
+ 'import { getChannelFolders } from "../repo/get-channel-folders.ts";',
1892
+ "",
1893
+ "interface ChannelFolderWithItems {",
1894
+ " folder: ChannelFolder;",
1895
+ " items: ChannelFolderItem[];",
1896
+ "}",
1897
+ "",
1898
+ "export const getChannelFoldersDomain = (): ChannelFolderWithItems[] => {",
1899
+ " return getChannelFolders();",
1900
+ "};",
1901
+ "",
1902
+ ].join("\n"), "utf-8");
1903
+ writeFileSync(join(dir, "packages", "channels", "src", "index.ts"), [
1904
+ 'export { ChannelFolder, ChannelFolderItem } from "./entities.ts";',
1905
+ 'export { getChannelFoldersDomain } from "./domain/get-channel-folders-domain.ts";',
1906
+ 'export { getChannelFolders } from "./repo/get-channel-folders.ts";',
1907
+ "",
1908
+ ].join("\n"), "utf-8");
1909
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
1910
+ 'import { getChannelFoldersDomain } from "@acme/channels/Acme.Channels.js";',
1911
+ "",
1912
+ "export function run(): string {",
1913
+ " const folders = getChannelFoldersDomain();",
1914
+ " const entry = folders[0];",
1915
+ ' if (entry === undefined) return "none";',
1916
+ ' return entry.folder.Id + ":" + entry.items[0]!.ChannelId;',
1917
+ "}",
1918
+ "",
1919
+ ].join("\n"), "utf-8");
1920
+ runProjectBuild(dir, wsConfigPath, "channels");
1921
+ const topLevelBindings = JSON.parse(readFileSync(join(dir, "packages", "channels", "dist", "tsonic", "bindings", "Acme.Channels", "bindings.json"), "utf-8"));
1922
+ expect(topLevelBindings.types?.some((type) => type.clrName === "Acme.Channels.domain.ChannelFolderWithItems" &&
1923
+ type.alias === "Acme.Channels.domain.ChannelFolderWithItems")).to.equal(true);
1924
+ expect(topLevelBindings.types?.some((type) => type.clrName === "Acme.Channels.repo.ChannelFolderWithItems" &&
1925
+ type.alias === "Acme.Channels.repo.ChannelFolderWithItems")).to.equal(true);
1926
+ linkDir(join(dir, "packages", "channels"), join(dir, "node_modules/@acme/channels"));
1927
+ runProjectBuild(dir, wsConfigPath, "app");
1928
+ }
1929
+ finally {
1930
+ rmSync(dir, { recursive: true, force: true });
1931
+ }
1932
+ });
1933
+ it("preserves callable const export signatures for sync await across source-package consumers", () => {
1934
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-await-sync-const-"));
1935
+ try {
1936
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
1937
+ mkdirSync(join(dir, "packages", "messages", "src", "domain"), {
1938
+ recursive: true,
1939
+ });
1940
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
1941
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
1942
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
1943
+ name: "test",
1944
+ private: true,
1945
+ type: "module",
1946
+ workspaces: ["packages/*"],
1947
+ }, null, 2) + "\n", "utf-8");
1948
+ writeFileSync(wsConfigPath, JSON.stringify({
1949
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
1950
+ dotnetVersion: "net10.0",
1951
+ }, null, 2) + "\n", "utf-8");
1952
+ writeFileSync(join(dir, "packages", "messages", "package.json"), JSON.stringify({
1953
+ name: "@acme/messages",
1954
+ private: true,
1955
+ type: "module",
1956
+ exports: {
1957
+ "./package.json": "./package.json",
1958
+ "./*.js": {
1959
+ types: "./dist/tsonic/bindings/*.d.ts",
1960
+ default: "./dist/tsonic/bindings/*.js",
1961
+ },
1962
+ },
1963
+ }, null, 2) + "\n", "utf-8");
1964
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
1965
+ name: "app",
1966
+ private: true,
1967
+ type: "module",
1968
+ }, null, 2) + "\n", "utf-8");
1969
+ writeFileSync(join(dir, "packages", "messages", "tsonic.json"), JSON.stringify({
1970
+ $schema: "https://tsonic.org/schema/v1.json",
1971
+ rootNamespace: "Acme.Messages",
1972
+ entryPoint: "src/index.ts",
1973
+ sourceRoot: "src",
1974
+ outputDirectory: "generated",
1975
+ outputName: "Acme.Messages",
1976
+ output: {
1977
+ type: "library",
1978
+ targetFrameworks: ["net10.0"],
1979
+ nativeAot: false,
1980
+ generateDocumentation: false,
1981
+ includeSymbols: false,
1982
+ packable: false,
1983
+ },
1984
+ }, null, 2) + "\n", "utf-8");
1985
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
1986
+ $schema: "https://tsonic.org/schema/v1.json",
1987
+ rootNamespace: "Acme.App",
1988
+ entryPoint: "src/App.ts",
1989
+ sourceRoot: "src",
1990
+ references: {
1991
+ libraries: [
1992
+ "../messages/generated/bin/Release/net10.0/Acme.Messages.dll",
1993
+ ],
1994
+ },
1995
+ outputDirectory: "generated",
1996
+ outputName: "Acme.App",
1997
+ output: {
1998
+ type: "library",
1999
+ targetFrameworks: ["net10.0"],
2000
+ nativeAot: false,
2001
+ generateDocumentation: false,
2002
+ includeSymbols: false,
2003
+ packable: false,
2004
+ },
2005
+ }, null, 2) + "\n", "utf-8");
2006
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
2007
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
2008
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
2009
+ writeFileSync(join(dir, "packages", "messages", "src", "index.ts"), [
2010
+ `export { renderMarkdownDomain } from "./domain/render-markdown-domain.ts";`,
2011
+ ``,
2012
+ ].join("\n"), "utf-8");
2013
+ writeFileSync(join(dir, "packages", "messages", "src", "domain", "render-markdown-domain.ts"), [
2014
+ `export type RenderResult =`,
2015
+ ` | { success: true; rendered: string }`,
2016
+ ` | { success: false; error: string };`,
2017
+ ``,
2018
+ `export const renderMarkdownDomain = (content: string): RenderResult => {`,
2019
+ ` if (content.Trim() === "") {`,
2020
+ ` return { success: false, error: "empty" };`,
2021
+ ` }`,
2022
+ ` return { success: true, rendered: content.ToUpper() };`,
2023
+ `};`,
2024
+ ``,
2025
+ ].join("\n"), "utf-8");
2026
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
2027
+ `import { renderMarkdownDomain } from "@acme/messages/Acme.Messages.js";`,
2028
+ ``,
2029
+ `export async function run(content: string): Promise<string> {`,
2030
+ ` const result = await renderMarkdownDomain(content);`,
2031
+ ` if (!result.success) {`,
2032
+ ` return result.error;`,
2033
+ ` }`,
2034
+ ` return result.rendered;`,
2035
+ `}`,
2036
+ ``,
2037
+ ].join("\n"), "utf-8");
2038
+ runProjectBuild(dir, wsConfigPath, "messages");
2039
+ const bindings = readFileSync(join(dir, "packages", "messages", "dist", "tsonic", "bindings", "Acme.Messages", "bindings.json"), "utf-8");
2040
+ expect(bindings).to.include('"kind": "functionType"');
2041
+ linkDir(join(dir, "packages", "messages"), join(dir, "node_modules/@acme/messages"));
2042
+ runProjectBuild(dir, wsConfigPath, "app");
2043
+ }
2044
+ finally {
2045
+ rmSync(dir, { recursive: true, force: true });
2046
+ }
2047
+ });
2048
+ it("preserves union-returning function signatures through source-package bindings", () => {
2049
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-union-return-"));
2050
+ try {
2051
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
2052
+ mkdirSync(join(dir, "packages", "queue", "src"), {
2053
+ recursive: true,
2054
+ });
2055
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
2056
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
2057
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
2058
+ name: "test",
2059
+ private: true,
2060
+ type: "module",
2061
+ workspaces: ["packages/*"],
2062
+ }, null, 2) + "\n", "utf-8");
2063
+ writeFileSync(wsConfigPath, JSON.stringify({
2064
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
2065
+ dotnetVersion: "net10.0",
2066
+ }, null, 2) + "\n", "utf-8");
2067
+ writeFileSync(join(dir, "packages", "queue", "package.json"), JSON.stringify({
2068
+ name: "@acme/queue",
2069
+ private: true,
2070
+ type: "module",
2071
+ exports: {
2072
+ "./package.json": "./package.json",
2073
+ "./*.js": {
2074
+ types: "./dist/tsonic/bindings/*.d.ts",
2075
+ default: "./dist/tsonic/bindings/*.js",
2076
+ },
2077
+ },
2078
+ }, null, 2) + "\n", "utf-8");
2079
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
2080
+ name: "app",
2081
+ private: true,
2082
+ type: "module",
2083
+ }, null, 2) + "\n", "utf-8");
2084
+ writeFileSync(join(dir, "packages", "queue", "tsonic.json"), JSON.stringify({
2085
+ $schema: "https://tsonic.org/schema/v1.json",
2086
+ rootNamespace: "Acme.Queue",
2087
+ entryPoint: "src/index.ts",
2088
+ sourceRoot: "src",
2089
+ outputDirectory: "generated",
2090
+ outputName: "Acme.Queue",
2091
+ output: {
2092
+ type: "library",
2093
+ targetFrameworks: ["net10.0"],
2094
+ nativeAot: false,
2095
+ generateDocumentation: false,
2096
+ includeSymbols: false,
2097
+ packable: false,
2098
+ },
2099
+ }, null, 2) + "\n", "utf-8");
2100
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
2101
+ $schema: "https://tsonic.org/schema/v1.json",
2102
+ rootNamespace: "Acme.App",
2103
+ entryPoint: "src/App.ts",
2104
+ sourceRoot: "src",
2105
+ references: {
2106
+ libraries: ["../queue/generated/bin/Release/net10.0/Acme.Queue.dll"],
2107
+ },
2108
+ outputDirectory: "generated",
2109
+ outputName: "Acme.App",
2110
+ output: {
2111
+ type: "library",
2112
+ targetFrameworks: ["net10.0"],
2113
+ nativeAot: false,
2114
+ generateDocumentation: false,
2115
+ includeSymbols: false,
2116
+ packable: false,
2117
+ },
2118
+ }, null, 2) + "\n", "utf-8");
2119
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
2120
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
2121
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
2122
+ writeFileSync(join(dir, "packages", "queue", "src", "index.ts"), [
2123
+ `export async function getEventsFromQueue(ok: boolean): Promise<{ events: string[] } | { error: string; code?: string }> {`,
2124
+ ` if (!ok) {`,
2125
+ ` return { error: "bad", code: "BAD_QUEUE" };`,
2126
+ ` }`,
2127
+ ` return { events: ["one", "two"] };`,
2128
+ `}`,
2129
+ ``,
2130
+ ].join("\n"), "utf-8");
2131
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
2132
+ `import { getEventsFromQueue } from "@acme/queue/Acme.Queue.js";`,
2133
+ ``,
2134
+ `export async function run(ok: boolean): Promise<string> {`,
2135
+ ` const result = await getEventsFromQueue(ok);`,
2136
+ ` if ("error" in result) {`,
2137
+ ` return result.code === undefined ? result.error : result.code + ":" + result.error;`,
2138
+ ` }`,
2139
+ ` return result.events[0] ?? "";`,
2140
+ `}`,
2141
+ ``,
2142
+ ].join("\n"), "utf-8");
2143
+ runProjectBuild(dir, wsConfigPath, "queue");
2144
+ const bindings = readFileSync(join(dir, "packages", "queue", "dist", "tsonic", "bindings", "Acme.Queue", "bindings.json"), "utf-8");
2145
+ expect(bindings).to.include('"semanticSignature"');
2146
+ expect(bindings).to.include('"kind": "unionType"');
2147
+ linkDir(join(dir, "packages", "queue"), join(dir, "node_modules/@acme/queue"));
2148
+ runProjectBuild(dir, wsConfigPath, "app");
2149
+ }
2150
+ finally {
2151
+ rmSync(dir, { recursive: true, force: true });
2152
+ }
2153
+ });
2154
+ it("preserves optional exact numerics through first-party bindings", () => {
2155
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-optional-int-"));
2156
+ try {
2157
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
2158
+ mkdirSync(join(dir, "packages", "users", "src"), {
2159
+ recursive: true,
2160
+ });
2161
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
2162
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
2163
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
2164
+ name: "test",
2165
+ private: true,
2166
+ type: "module",
2167
+ workspaces: ["packages/*"],
2168
+ }, null, 2) + "\n", "utf-8");
2169
+ writeFileSync(wsConfigPath, JSON.stringify({
2170
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
2171
+ dotnetVersion: "net10.0",
2172
+ }, null, 2) + "\n", "utf-8");
2173
+ writeFileSync(join(dir, "packages", "users", "package.json"), JSON.stringify({
2174
+ name: "@acme/users",
2175
+ private: true,
2176
+ type: "module",
2177
+ exports: {
2178
+ "./package.json": "./package.json",
2179
+ "./*.js": {
2180
+ types: "./dist/tsonic/bindings/*.d.ts",
2181
+ default: "./dist/tsonic/bindings/*.js",
2182
+ },
2183
+ },
2184
+ }, null, 2) + "\n", "utf-8");
2185
+ writeFileSync(join(dir, "packages", "app", "package.json"), JSON.stringify({
2186
+ name: "app",
2187
+ private: true,
2188
+ type: "module",
2189
+ }, null, 2) + "\n", "utf-8");
2190
+ writeFileSync(join(dir, "packages", "users", "tsonic.json"), JSON.stringify({
2191
+ $schema: "https://tsonic.org/schema/v1.json",
2192
+ rootNamespace: "Acme.Users",
2193
+ entryPoint: "src/index.ts",
2194
+ sourceRoot: "src",
2195
+ outputDirectory: "generated",
2196
+ outputName: "Acme.Users",
2197
+ output: {
2198
+ type: "library",
2199
+ targetFrameworks: ["net10.0"],
2200
+ nativeAot: false,
2201
+ generateDocumentation: false,
2202
+ includeSymbols: false,
2203
+ packable: false,
2204
+ },
2205
+ }, null, 2) + "\n", "utf-8");
2206
+ writeFileSync(join(dir, "packages", "app", "tsonic.json"), JSON.stringify({
2207
+ $schema: "https://tsonic.org/schema/v1.json",
2208
+ rootNamespace: "Acme.App",
2209
+ entryPoint: "src/App.ts",
2210
+ sourceRoot: "src",
2211
+ references: {
2212
+ libraries: ["../users/generated/bin/Release/net10.0/Acme.Users.dll"],
2213
+ },
2214
+ outputDirectory: "generated",
2215
+ outputName: "Acme.App",
2216
+ output: {
2217
+ type: "library",
2218
+ targetFrameworks: ["net10.0"],
2219
+ nativeAot: false,
2220
+ generateDocumentation: false,
2221
+ includeSymbols: false,
2222
+ packable: false,
2223
+ },
2224
+ }, null, 2) + "\n", "utf-8");
2225
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
2226
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
2227
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
2228
+ writeFileSync(join(dir, "packages", "users", "src", "index.ts"), [
2229
+ `import type { int } from "@tsonic/core/types.js";`,
2230
+ ``,
2231
+ `export interface CreateFieldInput {`,
2232
+ ` readonly name: string;`,
2233
+ ` readonly displayInProfileSummary?: int;`,
2234
+ `}`,
2235
+ ``,
2236
+ `export const createField = (input: CreateFieldInput): string => {`,
2237
+ ` return input.displayInProfileSummary === undefined ? input.name : input.name + input.displayInProfileSummary.ToString();`,
2238
+ `};`,
2239
+ ``,
2240
+ ].join("\n"), "utf-8");
2241
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
2242
+ `import type { int } from "@tsonic/core/types.js";`,
2243
+ `import { createField } from "@acme/users/Acme.Users.js";`,
2244
+ ``,
2245
+ `export function run(flag: boolean): string {`,
2246
+ ` const displayInProfileSummary: int | undefined = flag ? (1 as int) : undefined;`,
2247
+ ` return createField({ name: "field", displayInProfileSummary });`,
2248
+ `}`,
2249
+ ``,
2250
+ ].join("\n"), "utf-8");
2251
+ runProjectBuild(dir, wsConfigPath, "users");
2252
+ const bindings = readFileSync(join(dir, "packages", "users", "dist", "tsonic", "bindings", "Acme.Users", "bindings.json"), "utf-8");
2253
+ expect(bindings).to.include('"semanticOptional": true');
2254
+ expect(bindings).to.include('"name": "int"');
2255
+ linkDir(join(dir, "packages", "users"), join(dir, "node_modules/@acme/users"));
2256
+ runProjectBuild(dir, wsConfigPath, "app");
2257
+ }
2258
+ finally {
2259
+ rmSync(dir, { recursive: true, force: true });
2260
+ }
2261
+ });
2262
+ it("preserves imported canonical types and record element shapes across source-package consumers", () => {
2263
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-canonical-record-"));
2264
+ try {
2265
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
2266
+ mkdirSync(join(dir, "packages", "core", "src"), { recursive: true });
2267
+ mkdirSync(join(dir, "packages", "channels", "src"), { recursive: true });
2268
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
2269
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
2270
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
2271
+ name: "test",
2272
+ private: true,
2273
+ type: "module",
2274
+ workspaces: ["packages/*"],
2275
+ }, null, 2) + "\n", "utf-8");
2276
+ writeFileSync(wsConfigPath, JSON.stringify({
2277
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
2278
+ dotnetVersion: "net10.0",
2279
+ }, null, 2) + "\n", "utf-8");
2280
+ for (const [pkgName, namespace] of [
2281
+ ["core", "Acme.Core"],
2282
+ ["channels", "Acme.Channels"],
2283
+ ["app", "Acme.App"],
2284
+ ]) {
2285
+ writeFileSync(join(dir, "packages", pkgName, "package.json"), JSON.stringify(pkgName === "app"
2286
+ ? {
2287
+ name: "app",
2288
+ private: true,
2289
+ type: "module",
2290
+ }
2291
+ : {
2292
+ name: `@acme/${pkgName}`,
2293
+ private: true,
2294
+ type: "module",
2295
+ exports: {
2296
+ "./package.json": "./package.json",
2297
+ "./*.js": {
2298
+ types: "./dist/tsonic/bindings/*.d.ts",
2299
+ default: "./dist/tsonic/bindings/*.js",
2300
+ },
2301
+ },
2302
+ }, null, 2) + "\n", "utf-8");
2303
+ writeFileSync(join(dir, "packages", pkgName, "tsonic.json"), JSON.stringify({
2304
+ $schema: "https://tsonic.org/schema/v1.json",
2305
+ rootNamespace: namespace,
2306
+ entryPoint: pkgName === "app" ? "src/App.ts" : "src/index.ts",
2307
+ sourceRoot: "src",
2308
+ references: pkgName === "channels"
2309
+ ? {
2310
+ libraries: [
2311
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
2312
+ ],
2313
+ }
2314
+ : pkgName === "app"
2315
+ ? {
2316
+ libraries: [
2317
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
2318
+ "../channels/generated/bin/Release/net10.0/Acme.Channels.dll",
2319
+ ],
2320
+ }
2321
+ : undefined,
2322
+ outputDirectory: "generated",
2323
+ outputName: namespace,
2324
+ output: {
2325
+ type: "library",
2326
+ targetFrameworks: ["net10.0"],
2327
+ nativeAot: false,
2328
+ generateDocumentation: false,
2329
+ includeSymbols: false,
2330
+ packable: false,
2331
+ },
2332
+ }, null, 2) + "\n", "utf-8");
2333
+ }
2334
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
2335
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
2336
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
2337
+ writeFileSync(join(dir, "packages", "core", "src", "index.ts"), [
2338
+ `export class ChannelFolderItem {`,
2339
+ ` ChannelId: string = "";`,
2340
+ `}`,
2341
+ ``,
2342
+ `export class Channel {`,
2343
+ ` Id: string = "";`,
2344
+ ` Name: string = "";`,
2345
+ `}`,
2346
+ ``,
2347
+ ].join("\n"), "utf-8");
2348
+ writeFileSync(join(dir, "packages", "channels", "src", "index.ts"), [
2349
+ `import { Channel, ChannelFolderItem } from "@acme/core/Acme.Core.js";`,
2350
+ ``,
2351
+ `export interface ChannelFolderWithItems {`,
2352
+ ` readonly items: ChannelFolderItem[];`,
2353
+ `}`,
2354
+ ``,
2355
+ `export const getChannelFoldersDomain = (): ChannelFolderWithItems[] => {`,
2356
+ ` const item = new ChannelFolderItem();`,
2357
+ ` item.ChannelId = "chan-1";`,
2358
+ ` return [{ items: [item] }];`,
2359
+ `};`,
2360
+ ``,
2361
+ `export const getAllChannels = (): Channel[] => {`,
2362
+ ` const channel = new Channel();`,
2363
+ ` channel.Id = "chan-1";`,
2364
+ ` channel.Name = "General";`,
2365
+ ` return [channel];`,
2366
+ `};`,
2367
+ ``,
2368
+ ].join("\n"), "utf-8");
2369
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
2370
+ `import { getAllChannels, getChannelFoldersDomain } from "@acme/channels/Acme.Channels.js";`,
2371
+ ``,
2372
+ `export function run(): string {`,
2373
+ ` const folders = getChannelFoldersDomain();`,
2374
+ ` const entry = folders[0];`,
2375
+ ` if (entry === undefined) return "none";`,
2376
+ ` const allChannels = getAllChannels();`,
2377
+ ` const channelMap: Record<string, typeof allChannels[0]> = {};`,
2378
+ ` for (let i = 0; i < allChannels.Length; i++) {`,
2379
+ ` const channel = allChannels[i];`,
2380
+ ` if (channel !== undefined) {`,
2381
+ ` channelMap[channel.Id] = channel;`,
2382
+ ` }`,
2383
+ ` }`,
2384
+ ` const mapped = channelMap[entry.items[0]!.ChannelId];`,
2385
+ ` return entry.items[0]!.ChannelId + ":" + (mapped === undefined ? "missing" : mapped.Name);`,
2386
+ `}`,
2387
+ ``,
2388
+ ].join("\n"), "utf-8");
2389
+ runProjectBuild(dir, wsConfigPath, "core");
2390
+ linkDir(join(dir, "packages", "core"), join(dir, "node_modules/@acme/core"));
2391
+ runProjectBuild(dir, wsConfigPath, "channels");
2392
+ linkDir(join(dir, "packages", "channels"), join(dir, "node_modules/@acme/channels"));
2393
+ runProjectBuild(dir, wsConfigPath, "app");
2394
+ const emitted = readFileSync(join(dir, "packages", "app", "generated", "App.cs"), "utf-8");
2395
+ expect(emitted).to.not.include("global::ChannelFolderItem");
2396
+ expect(emitted).to.not.include("Dictionary<string, object?> channelMap");
2397
+ }
2398
+ finally {
2399
+ rmSync(dir, { recursive: true, force: true });
2400
+ }
2401
+ });
2402
+ it("preserves indexed-access structural members through source-package declaration bindings", () => {
2403
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-indexed-access-"));
2404
+ try {
2405
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
2406
+ mkdirSync(join(dir, "packages", "events", "src"), { recursive: true });
2407
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
2408
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
2409
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
2410
+ name: "test",
2411
+ private: true,
2412
+ type: "module",
2413
+ workspaces: ["packages/*"],
2414
+ }, null, 2) + "\n", "utf-8");
2415
+ writeFileSync(wsConfigPath, JSON.stringify({
2416
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
2417
+ dotnetVersion: "net10.0",
2418
+ surface: "@tsonic/js",
2419
+ }, null, 2) + "\n", "utf-8");
2420
+ for (const [pkgName, namespace] of [
2421
+ ["events", "Acme.Events"],
2422
+ ["app", "Acme.App"],
2423
+ ]) {
2424
+ writeFileSync(join(dir, "packages", pkgName, "package.json"), JSON.stringify(pkgName === "app"
2425
+ ? {
2426
+ name: "app",
2427
+ private: true,
2428
+ type: "module",
2429
+ }
2430
+ : {
2431
+ name: `@acme/${pkgName}`,
2432
+ private: true,
2433
+ type: "module",
2434
+ exports: {
2435
+ "./package.json": "./package.json",
2436
+ "./*.js": {
2437
+ types: "./dist/tsonic/bindings/*.d.ts",
2438
+ default: "./dist/tsonic/bindings/*.js",
2439
+ },
2440
+ },
2441
+ }, null, 2) + "\n", "utf-8");
2442
+ writeFileSync(join(dir, "packages", pkgName, "tsonic.json"), JSON.stringify({
2443
+ $schema: "https://tsonic.org/schema/v1.json",
2444
+ rootNamespace: namespace,
2445
+ entryPoint: pkgName === "app" ? "src/App.ts" : "src/index.ts",
2446
+ sourceRoot: "src",
2447
+ references: pkgName === "app"
2448
+ ? {
2449
+ libraries: [
2450
+ "../events/generated/bin/Release/net10.0/Acme.Events.dll",
2451
+ ],
2452
+ }
2453
+ : undefined,
2454
+ outputDirectory: "generated",
2455
+ outputName: namespace,
2456
+ output: {
2457
+ type: "library",
2458
+ targetFrameworks: ["net10.0"],
2459
+ nativeAot: false,
2460
+ generateDocumentation: false,
2461
+ includeSymbols: false,
2462
+ packable: false,
2463
+ },
2464
+ }, null, 2) + "\n", "utf-8");
2465
+ }
2466
+ writeFileSync(join(dir, "packages", "events", "src", "index.ts"), [
2467
+ "export interface ClientCapabilities {",
2468
+ " notificationBadge: boolean;",
2469
+ "}",
2470
+ "",
2471
+ "export interface RegisterParams {",
2472
+ " clientCapabilities?: ClientCapabilities;",
2473
+ " narrow?: { operator: string; operand: string; negated?: boolean }[];",
2474
+ "}",
2475
+ "",
2476
+ ].join("\n"), "utf-8");
2477
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
2478
+ 'import type { RegisterParams } from "@acme/events/Acme.Events.js";',
2479
+ "",
2480
+ "export function run(",
2481
+ " clientCapabilitiesRaw: string | undefined,",
2482
+ " narrowRaw: string | undefined",
2483
+ "): RegisterParams {",
2484
+ " const clientCapabilities = clientCapabilitiesRaw",
2485
+ " ? (JSON.parse(clientCapabilitiesRaw) as RegisterParams[\"clientCapabilities\"])",
2486
+ " : undefined;",
2487
+ " const narrow = narrowRaw",
2488
+ " ? (JSON.parse(narrowRaw) as RegisterParams[\"narrow\"])",
2489
+ " : undefined;",
2490
+ " return { clientCapabilities, narrow };",
2491
+ "}",
2492
+ "",
2493
+ ].join("\n"), "utf-8");
2494
+ runProjectBuild(dir, wsConfigPath, "events");
2495
+ linkDir(join(dir, "packages", "events"), join(dir, "node_modules/@acme/events"));
2496
+ runProjectBuild(dir, wsConfigPath, "app");
2497
+ const generated = readFileSync(join(dir, "packages", "app", "generated", "App.cs"), "utf-8");
2498
+ expect(generated).to.not.include("JSON.parse<object>");
2499
+ expect(generated).to.include("ClientCapabilities");
2500
+ expect(generated).to.include("operator");
2501
+ expect(generated).to.include("operand");
2502
+ }
2503
+ finally {
2504
+ rmSync(dir, { recursive: true, force: true });
2505
+ }
2506
+ });
2507
+ it("preserves awaited imported array element shapes inside record value types", () => {
2508
+ const dir = mkdtempSync(join(tmpdir(), "tsonic-lib-bindings-awaited-record-"));
2509
+ try {
2510
+ const wsConfigPath = join(dir, "tsonic.workspace.json");
2511
+ mkdirSync(join(dir, "packages", "core", "src"), { recursive: true });
2512
+ mkdirSync(join(dir, "packages", "channels", "src"), { recursive: true });
2513
+ mkdirSync(join(dir, "packages", "app", "src"), { recursive: true });
2514
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
2515
+ writeFileSync(join(dir, "package.json"), JSON.stringify({
2516
+ name: "test",
2517
+ private: true,
2518
+ type: "module",
2519
+ workspaces: ["packages/*"],
2520
+ }, null, 2) + "\n", "utf-8");
2521
+ writeFileSync(wsConfigPath, JSON.stringify({
2522
+ $schema: "https://tsonic.org/schema/workspace/v1.json",
2523
+ dotnetVersion: "net10.0",
2524
+ }, null, 2) + "\n", "utf-8");
2525
+ for (const [pkgName, namespace] of [
2526
+ ["core", "Acme.Core"],
2527
+ ["channels", "Acme.Channels"],
2528
+ ["app", "Acme.App"],
2529
+ ]) {
2530
+ writeFileSync(join(dir, "packages", pkgName, "package.json"), JSON.stringify(pkgName === "app"
2531
+ ? {
2532
+ name: "app",
2533
+ private: true,
2534
+ type: "module",
2535
+ }
2536
+ : {
2537
+ name: `@acme/${pkgName}`,
2538
+ private: true,
2539
+ type: "module",
2540
+ exports: {
2541
+ "./package.json": "./package.json",
2542
+ "./*.js": {
2543
+ types: "./dist/tsonic/bindings/*.d.ts",
2544
+ default: "./dist/tsonic/bindings/*.js",
2545
+ },
2546
+ },
2547
+ }, null, 2) + "\n", "utf-8");
2548
+ writeFileSync(join(dir, "packages", pkgName, "tsonic.json"), JSON.stringify({
2549
+ $schema: "https://tsonic.org/schema/v1.json",
2550
+ rootNamespace: namespace,
2551
+ entryPoint: pkgName === "app" ? "src/App.ts" : "src/index.ts",
2552
+ sourceRoot: "src",
2553
+ references: pkgName === "channels"
2554
+ ? {
2555
+ libraries: [
2556
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
2557
+ ],
2558
+ }
2559
+ : pkgName === "app"
2560
+ ? {
2561
+ libraries: [
2562
+ "../core/generated/bin/Release/net10.0/Acme.Core.dll",
2563
+ "../channels/generated/bin/Release/net10.0/Acme.Channels.dll",
2564
+ ],
2565
+ }
2566
+ : undefined,
2567
+ outputDirectory: "generated",
2568
+ outputName: namespace,
2569
+ output: {
2570
+ type: "library",
2571
+ targetFrameworks: ["net10.0"],
2572
+ nativeAot: false,
2573
+ generateDocumentation: false,
2574
+ includeSymbols: false,
2575
+ packable: false,
2576
+ },
2577
+ }, null, 2) + "\n", "utf-8");
2578
+ }
2579
+ linkDir(join(repoRoot, "node_modules/@tsonic/dotnet"), join(dir, "node_modules/@tsonic/dotnet"));
2580
+ linkDir(join(repoRoot, "node_modules/@tsonic/core"), join(dir, "node_modules/@tsonic/core"));
2581
+ linkDir(join(repoRoot, "node_modules/@tsonic/globals"), join(dir, "node_modules/@tsonic/globals"));
2582
+ writeFileSync(join(dir, "packages", "core", "src", "index.ts"), [
2583
+ `export class Channel {`,
2584
+ ` Id: string = "";`,
2585
+ ` Name: string = "";`,
2586
+ ` Description: string = "";`,
2587
+ `}`,
2588
+ ``,
2589
+ ].join("\n"), "utf-8");
2590
+ writeFileSync(join(dir, "packages", "channels", "src", "index.ts"), [
2591
+ `import { Channel } from "@acme/core/Acme.Core.js";`,
2592
+ ``,
2593
+ `export const getChannels = async (): Promise<Channel[]> => {`,
2594
+ ` const channel = new Channel();`,
2595
+ ` channel.Id = "chan-1";`,
2596
+ ` channel.Name = "General";`,
2597
+ ` channel.Description = "Main";`,
2598
+ ` return [channel];`,
2599
+ `};`,
2600
+ ``,
2601
+ ].join("\n"), "utf-8");
2602
+ writeFileSync(join(dir, "packages", "app", "src", "App.ts"), [
2603
+ `import { getChannels } from "@acme/channels/Acme.Channels.js";`,
2604
+ ``,
2605
+ `export async function run(): Promise<string> {`,
2606
+ ` const allChannels = await getChannels();`,
2607
+ ` const channelMap: Record<string, typeof allChannels[0]> = {};`,
2608
+ ` for (let i = 0; i < allChannels.length; i++) {`,
2609
+ ` const channel = allChannels[i];`,
2610
+ ` if (channel !== undefined) {`,
2611
+ ` channelMap[channel.Id] = channel;`,
2612
+ ` }`,
2613
+ ` }`,
2614
+ ` const mapped = channelMap["chan-1"];`,
2615
+ ` return mapped === undefined ? "missing" : mapped.Name + ":" + mapped.Description;`,
2616
+ `}`,
2617
+ ``,
2618
+ ].join("\n"), "utf-8");
2619
+ runProjectBuild(dir, wsConfigPath, "core");
2620
+ linkDir(join(dir, "packages", "core"), join(dir, "node_modules/@acme/core"));
2621
+ runProjectBuild(dir, wsConfigPath, "channels");
2622
+ linkDir(join(dir, "packages", "channels"), join(dir, "node_modules/@acme/channels"));
2623
+ runProjectBuild(dir, wsConfigPath, "app");
2624
+ const emitted = readFileSync(join(dir, "packages", "app", "generated", "App.cs"), "utf-8");
2625
+ expect(emitted).to.include("global::System.Collections.Generic.Dictionary<string, global::Acme.Core.Channel>");
2626
+ expect(emitted).to.include('return mapped == null ? "missing" : mapped.Name + ":" + mapped.Description;');
2627
+ expect(emitted).to.not.include("global::System.Collections.Generic.Dictionary<string, object?>");
2628
+ }
2629
+ finally {
2630
+ rmSync(dir, { recursive: true, force: true });
2631
+ }
2632
+ });
213
2633
  });
214
2634
  //# sourceMappingURL=library-bindings-firstparty-regressions.test.js.map