@tsonic/cli 0.0.72 → 0.0.74

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