@tsonic/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/cli/constants.d.ts +5 -0
  2. package/dist/cli/constants.d.ts.map +1 -0
  3. package/dist/cli/constants.js +5 -0
  4. package/dist/cli/constants.js.map +1 -0
  5. package/dist/cli/dispatcher.d.ts +8 -0
  6. package/dist/cli/dispatcher.d.ts.map +1 -0
  7. package/dist/cli/dispatcher.js +111 -0
  8. package/dist/cli/dispatcher.js.map +1 -0
  9. package/dist/cli/help.d.ts +8 -0
  10. package/dist/cli/help.d.ts.map +1 -0
  11. package/dist/cli/help.js +55 -0
  12. package/dist/cli/help.js.map +1 -0
  13. package/dist/cli/index.d.ts +8 -0
  14. package/dist/cli/index.d.ts.map +1 -0
  15. package/dist/cli/index.js +8 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/parser.d.ts +14 -0
  18. package/dist/cli/parser.d.ts.map +1 -0
  19. package/dist/cli/parser.js +113 -0
  20. package/dist/cli/parser.js.map +1 -0
  21. package/dist/cli/parser.test.d.ts +5 -0
  22. package/dist/cli/parser.test.d.ts.map +1 -0
  23. package/dist/cli/parser.test.js +221 -0
  24. package/dist/cli/parser.test.js.map +1 -0
  25. package/dist/cli.d.ts +6 -0
  26. package/dist/cli.d.ts.map +1 -0
  27. package/dist/cli.js +6 -0
  28. package/dist/cli.js.map +1 -0
  29. package/dist/commands/build.d.ts +11 -0
  30. package/dist/commands/build.d.ts.map +1 -0
  31. package/dist/commands/build.js +214 -0
  32. package/dist/commands/build.js.map +1 -0
  33. package/dist/commands/emit.d.ts +12 -0
  34. package/dist/commands/emit.d.ts.map +1 -0
  35. package/dist/commands/emit.js +311 -0
  36. package/dist/commands/emit.js.map +1 -0
  37. package/dist/commands/init.d.ts +15 -0
  38. package/dist/commands/init.d.ts.map +1 -0
  39. package/dist/commands/init.js +270 -0
  40. package/dist/commands/init.js.map +1 -0
  41. package/dist/commands/pack.d.ts +11 -0
  42. package/dist/commands/pack.d.ts.map +1 -0
  43. package/dist/commands/pack.js +92 -0
  44. package/dist/commands/pack.js.map +1 -0
  45. package/dist/commands/run.d.ts +11 -0
  46. package/dist/commands/run.d.ts.map +1 -0
  47. package/dist/commands/run.js +40 -0
  48. package/dist/commands/run.js.map +1 -0
  49. package/dist/config.d.ts +17 -0
  50. package/dist/config.d.ts.map +1 -0
  51. package/dist/config.js +160 -0
  52. package/dist/config.js.map +1 -0
  53. package/dist/config.test.d.ts +5 -0
  54. package/dist/config.test.d.ts.map +1 -0
  55. package/dist/config.test.js +283 -0
  56. package/dist/config.test.js.map +1 -0
  57. package/dist/index.d.ts +8 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +20 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/types.d.ts +111 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +5 -0
  64. package/dist/types.js.map +1 -0
  65. package/package.json +38 -0
  66. package/src/cli/constants.ts +5 -0
  67. package/src/cli/dispatcher.ts +129 -0
  68. package/src/cli/help.ts +56 -0
  69. package/src/cli/index.ts +8 -0
  70. package/src/cli/parser.test.ts +259 -0
  71. package/src/cli/parser.ts +128 -0
  72. package/src/cli.ts +6 -0
  73. package/src/commands/build.ts +264 -0
  74. package/src/commands/emit.ts +406 -0
  75. package/src/commands/init.ts +334 -0
  76. package/src/commands/pack.ts +114 -0
  77. package/src/commands/run.ts +51 -0
  78. package/src/config.test.ts +337 -0
  79. package/src/config.ts +205 -0
  80. package/src/index.ts +23 -0
  81. package/src/types.ts +121 -0
  82. package/tsconfig.json +18 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * tsonic build command - Build executable or library
3
+ */
4
+
5
+ import { spawnSync } from "node:child_process";
6
+ import { join, relative } from "node:path";
7
+ import { copyFileSync, chmodSync, existsSync, mkdirSync } from "node:fs";
8
+ import type { ResolvedConfig, Result } from "../types.js";
9
+ import { emitCommand } from "./emit.js";
10
+
11
+ /**
12
+ * Build native executable
13
+ */
14
+ const buildExecutable = (
15
+ config: ResolvedConfig,
16
+ generatedDir: string
17
+ ): Result<{ outputPath: string }, string> => {
18
+ const { outputName, rid, quiet, verbose } = config;
19
+
20
+ // Step 2: Run dotnet publish
21
+ if (!quiet) {
22
+ console.log("Step 2/3: Compiling with dotnet publish...");
23
+ }
24
+
25
+ const publishArgs = [
26
+ "publish",
27
+ "tsonic.csproj",
28
+ "-c",
29
+ "Release",
30
+ "-r",
31
+ rid,
32
+ "--nologo",
33
+ ];
34
+
35
+ if (quiet) {
36
+ publishArgs.push("--verbosity", "quiet");
37
+ } else if (verbose) {
38
+ publishArgs.push("--verbosity", "detailed");
39
+ } else {
40
+ publishArgs.push("--verbosity", "minimal");
41
+ }
42
+
43
+ const publishResult = spawnSync("dotnet", publishArgs, {
44
+ cwd: generatedDir,
45
+ stdio: verbose ? "inherit" : "pipe",
46
+ encoding: "utf-8",
47
+ });
48
+
49
+ if (publishResult.status !== 0) {
50
+ const errorMsg =
51
+ publishResult.stderr || publishResult.stdout || "Unknown error";
52
+ return {
53
+ ok: false,
54
+ error: `dotnet publish failed:\n${errorMsg}`,
55
+ };
56
+ }
57
+
58
+ // Step 3: Copy output binary
59
+ if (!quiet) {
60
+ console.log("Step 3/3: Copying output binary...");
61
+ }
62
+
63
+ const binaryName =
64
+ process.platform === "win32" ? `${outputName}.exe` : outputName;
65
+ const publishDir = join(
66
+ generatedDir,
67
+ "bin",
68
+ "Release",
69
+ config.dotnetVersion,
70
+ rid,
71
+ "publish"
72
+ );
73
+ const sourceBinary = join(publishDir, binaryName);
74
+ const targetBinary = join(process.cwd(), binaryName);
75
+
76
+ if (!existsSync(sourceBinary)) {
77
+ return {
78
+ ok: false,
79
+ error: `Built binary not found at ${sourceBinary}`,
80
+ };
81
+ }
82
+
83
+ try {
84
+ copyFileSync(sourceBinary, targetBinary);
85
+
86
+ // Make executable on Unix
87
+ if (process.platform !== "win32") {
88
+ chmodSync(targetBinary, 0o755);
89
+ }
90
+
91
+ if (!quiet) {
92
+ const relativePath = relative(process.cwd(), targetBinary);
93
+ console.log(`\n✓ Build complete: ${relativePath}`);
94
+ }
95
+
96
+ return {
97
+ ok: true,
98
+ value: { outputPath: targetBinary },
99
+ };
100
+ } catch (error) {
101
+ return {
102
+ ok: false,
103
+ error: `Failed to copy binary: ${error instanceof Error ? error.message : String(error)}`,
104
+ };
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Build library
110
+ */
111
+ const buildLibrary = (
112
+ config: ResolvedConfig,
113
+ generatedDir: string
114
+ ): Result<{ outputPath: string }, string> => {
115
+ const { outputName, quiet, verbose } = config;
116
+ const targetFrameworks = config.outputConfig.targetFrameworks ?? [
117
+ config.dotnetVersion,
118
+ ];
119
+
120
+ // Step 2: Run dotnet build
121
+ if (!quiet) {
122
+ console.log("Step 2/3: Compiling library with dotnet build...");
123
+ }
124
+
125
+ const buildArgs = ["build", "tsonic.csproj", "-c", "Release", "--nologo"];
126
+
127
+ if (quiet) {
128
+ buildArgs.push("--verbosity", "quiet");
129
+ } else if (verbose) {
130
+ buildArgs.push("--verbosity", "detailed");
131
+ } else {
132
+ buildArgs.push("--verbosity", "minimal");
133
+ }
134
+
135
+ const buildResult = spawnSync("dotnet", buildArgs, {
136
+ cwd: generatedDir,
137
+ stdio: verbose ? "inherit" : "pipe",
138
+ encoding: "utf-8",
139
+ });
140
+
141
+ if (buildResult.status !== 0) {
142
+ const errorMsg =
143
+ buildResult.stderr || buildResult.stdout || "Unknown error";
144
+ return {
145
+ ok: false,
146
+ error: `dotnet build failed:\n${errorMsg}`,
147
+ };
148
+ }
149
+
150
+ // Step 3: Copy output library artifacts
151
+ if (!quiet) {
152
+ console.log("Step 3/3: Copying library artifacts...");
153
+ }
154
+
155
+ const outputDir = join(process.cwd(), "dist");
156
+
157
+ try {
158
+ // Create output directory
159
+ if (!existsSync(outputDir)) {
160
+ mkdirSync(outputDir, { recursive: true });
161
+ }
162
+
163
+ // Copy artifacts for each target framework
164
+ const copiedFiles: string[] = [];
165
+
166
+ for (const framework of targetFrameworks) {
167
+ const buildDir = join(generatedDir, "bin", "Release", framework);
168
+
169
+ if (!existsSync(buildDir)) {
170
+ continue;
171
+ }
172
+
173
+ const frameworkOutputDir = join(outputDir, framework);
174
+ if (!existsSync(frameworkOutputDir)) {
175
+ mkdirSync(frameworkOutputDir, { recursive: true });
176
+ }
177
+
178
+ // Copy .dll
179
+ const dllSource = join(buildDir, `${outputName}.dll`);
180
+ if (existsSync(dllSource)) {
181
+ const dllTarget = join(frameworkOutputDir, `${outputName}.dll`);
182
+ copyFileSync(dllSource, dllTarget);
183
+ copiedFiles.push(relative(process.cwd(), dllTarget));
184
+ }
185
+
186
+ // Copy .xml (documentation)
187
+ const xmlSource = join(buildDir, `${outputName}.xml`);
188
+ if (existsSync(xmlSource)) {
189
+ const xmlTarget = join(frameworkOutputDir, `${outputName}.xml`);
190
+ copyFileSync(xmlSource, xmlTarget);
191
+ copiedFiles.push(relative(process.cwd(), xmlTarget));
192
+ }
193
+
194
+ // Copy .pdb (symbols)
195
+ const pdbSource = join(buildDir, `${outputName}.pdb`);
196
+ if (existsSync(pdbSource)) {
197
+ const pdbTarget = join(frameworkOutputDir, `${outputName}.pdb`);
198
+ copyFileSync(pdbSource, pdbTarget);
199
+ copiedFiles.push(relative(process.cwd(), pdbTarget));
200
+ }
201
+ }
202
+
203
+ if (copiedFiles.length === 0) {
204
+ return {
205
+ ok: false,
206
+ error: "No library artifacts found to copy",
207
+ };
208
+ }
209
+
210
+ if (!quiet) {
211
+ console.log(`\n✓ Build complete. Artifacts copied to dist/:`);
212
+ for (const file of copiedFiles) {
213
+ console.log(` - ${file}`);
214
+ }
215
+ }
216
+
217
+ return {
218
+ ok: true,
219
+ value: { outputPath: outputDir },
220
+ };
221
+ } catch (error) {
222
+ return {
223
+ ok: false,
224
+ error: `Failed to copy library artifacts: ${error instanceof Error ? error.message : String(error)}`,
225
+ };
226
+ }
227
+ };
228
+
229
+ /**
230
+ * Main build command - dispatches to executable or library build
231
+ */
232
+ export const buildCommand = (
233
+ config: ResolvedConfig
234
+ ): Result<{ outputPath: string }, string> => {
235
+ const { outputDirectory, quiet } = config;
236
+ const outputType = config.outputConfig.type ?? "executable";
237
+
238
+ // Step 1: Emit C# code
239
+ if (!quiet) {
240
+ console.log("Step 1/3: Generating C# code...");
241
+ }
242
+
243
+ const emitResult = emitCommand(config);
244
+ if (!emitResult.ok) {
245
+ return emitResult;
246
+ }
247
+
248
+ const generatedDir = emitResult.value.outputDir;
249
+ const csprojPath = join(generatedDir, "tsonic.csproj");
250
+
251
+ if (!existsSync(csprojPath)) {
252
+ return {
253
+ ok: false,
254
+ error: `No tsonic.csproj found in ${outputDirectory}/. This should have been created by emit.`,
255
+ };
256
+ }
257
+
258
+ // Dispatch to appropriate build function
259
+ if (outputType === "library") {
260
+ return buildLibrary(config, generatedDir);
261
+ } else {
262
+ return buildExecutable(config, generatedDir);
263
+ }
264
+ };
@@ -0,0 +1,406 @@
1
+ /**
2
+ * tsonic emit command - Generate C# code only
3
+ */
4
+
5
+ import {
6
+ mkdirSync,
7
+ writeFileSync,
8
+ existsSync,
9
+ readdirSync,
10
+ copyFileSync,
11
+ } from "node:fs";
12
+ import { join, dirname, relative, resolve } from "node:path";
13
+ import {
14
+ buildModuleDependencyGraph,
15
+ type Diagnostic,
16
+ type IrModule,
17
+ type CompilerOptions,
18
+ } from "@tsonic/frontend";
19
+ import { emitCSharpFiles } from "@tsonic/emitter";
20
+ import {
21
+ generateCsproj,
22
+ generateProgramCs,
23
+ type EntryInfo,
24
+ type BuildConfig,
25
+ type ExecutableConfig,
26
+ type LibraryConfig,
27
+ type AssemblyReference,
28
+ } from "@tsonic/backend";
29
+ import type { ResolvedConfig, Result } from "../types.js";
30
+
31
+ /**
32
+ * Find project .csproj file in current directory
33
+ */
34
+ const findProjectCsproj = (): string | null => {
35
+ const cwd = process.cwd();
36
+ const files = readdirSync(cwd);
37
+ const csprojFile = files.find((f) => f.endsWith(".csproj"));
38
+ return csprojFile ? join(cwd, csprojFile) : null;
39
+ };
40
+
41
+ /**
42
+ * Find runtime DLLs from @tsonic/tsonic npm package
43
+ * Returns assembly references for the csproj file
44
+ */
45
+ const findRuntimeDlls = (
46
+ runtime: "js" | "dotnet",
47
+ outputDir: string
48
+ ): readonly AssemblyReference[] => {
49
+ // Try to find @tsonic/tsonic package runtime directory
50
+ const possiblePaths = [
51
+ // From project's node_modules
52
+ join(process.cwd(), "node_modules/@tsonic/tsonic/runtime"),
53
+ // From CLI's node_modules (when installed globally or via npx)
54
+ join(import.meta.dirname, "../../runtime"),
55
+ join(import.meta.dirname, "../../../runtime"),
56
+ ];
57
+
58
+ let runtimeDir: string | null = null;
59
+ for (const p of possiblePaths) {
60
+ if (existsSync(p)) {
61
+ runtimeDir = p;
62
+ break;
63
+ }
64
+ }
65
+
66
+ if (!runtimeDir) {
67
+ return [];
68
+ }
69
+
70
+ const refs: AssemblyReference[] = [];
71
+
72
+ // Calculate relative path from output directory to runtime directory
73
+ const relativeRuntimeDir = relative(outputDir, runtimeDir);
74
+
75
+ // Always include Tsonic.Runtime
76
+ const runtimeDll = join(runtimeDir, "Tsonic.Runtime.dll");
77
+ if (existsSync(runtimeDll)) {
78
+ refs.push({
79
+ name: "Tsonic.Runtime",
80
+ hintPath: join(relativeRuntimeDir, "Tsonic.Runtime.dll"),
81
+ });
82
+ }
83
+
84
+ // Include Tsonic.JSRuntime for js mode
85
+ if (runtime === "js") {
86
+ const jsRuntimeDll = join(runtimeDir, "Tsonic.JSRuntime.dll");
87
+ if (existsSync(jsRuntimeDll)) {
88
+ refs.push({
89
+ name: "Tsonic.JSRuntime",
90
+ hintPath: join(relativeRuntimeDir, "Tsonic.JSRuntime.dll"),
91
+ });
92
+ }
93
+ }
94
+
95
+ return refs;
96
+ };
97
+
98
+ /**
99
+ * Extract entry point information from IR module
100
+ */
101
+ const extractEntryInfo = (
102
+ entryModule: IrModule,
103
+ runtime?: "js" | "dotnet"
104
+ ): EntryInfo | null => {
105
+ // Look for exported 'main' function
106
+ for (const exp of entryModule.exports) {
107
+ if (exp.kind === "declaration") {
108
+ const decl = exp.declaration;
109
+ if (decl.kind === "functionDeclaration" && decl.name === "main") {
110
+ return {
111
+ namespace: entryModule.namespace,
112
+ className: entryModule.className,
113
+ methodName: "main",
114
+ isAsync: decl.isAsync,
115
+ needsProgram: true,
116
+ runtime,
117
+ };
118
+ }
119
+ } else if (exp.kind === "named" && exp.name === "main") {
120
+ // Named export of 'main'
121
+ // Look in body for the function declaration
122
+ for (const stmt of entryModule.body) {
123
+ if (stmt.kind === "functionDeclaration" && stmt.name === "main") {
124
+ return {
125
+ namespace: entryModule.namespace,
126
+ className: entryModule.className,
127
+ methodName: "main",
128
+ isAsync: stmt.isAsync,
129
+ needsProgram: true,
130
+ runtime,
131
+ };
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ // No main function found
138
+ return null;
139
+ };
140
+
141
+ /**
142
+ * Emit C# code from TypeScript
143
+ */
144
+ export const emitCommand = (
145
+ config: ResolvedConfig
146
+ ): Result<{ filesGenerated: number; outputDir: string }, string> => {
147
+ const {
148
+ entryPoint,
149
+ outputDirectory,
150
+ rootNamespace,
151
+ sourceRoot,
152
+ packages,
153
+ typeRoots,
154
+ } = config;
155
+
156
+ // For libraries, entry point is optional
157
+ if (!entryPoint && config.outputConfig.type !== "library") {
158
+ return {
159
+ ok: false,
160
+ error: "Entry point is required for executable builds",
161
+ };
162
+ }
163
+
164
+ if (!config.quiet) {
165
+ const target = entryPoint ?? sourceRoot;
166
+ console.log(`Emitting C# code for ${target}...`);
167
+ }
168
+
169
+ try {
170
+ // For libraries without entry point, we need a different approach
171
+ // For now, require entry point (library multi-file support can be added later)
172
+ if (!entryPoint) {
173
+ return {
174
+ ok: false,
175
+ error:
176
+ "Entry point is required (library multi-file support coming soon)",
177
+ };
178
+ }
179
+
180
+ // Combine typeRoots and libraries for TypeScript compilation
181
+ const allTypeRoots = [...typeRoots, ...config.libraries];
182
+
183
+ // Build dependency graph - this traverses all imports and builds IR for all modules
184
+ const compilerOptions: CompilerOptions = {
185
+ sourceRoot,
186
+ rootNamespace,
187
+ typeRoots: allTypeRoots,
188
+ verbose: config.verbose,
189
+ };
190
+ const graphResult = buildModuleDependencyGraph(entryPoint, compilerOptions);
191
+
192
+ if (!graphResult.ok) {
193
+ const errorMessages = graphResult.error
194
+ .map((d: Diagnostic) => {
195
+ if (d.location) {
196
+ return `${d.location.file}:${d.location.line} ${d.message}`;
197
+ }
198
+ return d.message;
199
+ })
200
+ .join("\n");
201
+ return {
202
+ ok: false,
203
+ error: `TypeScript compilation failed:\n${errorMessages}`,
204
+ };
205
+ }
206
+
207
+ const { modules, entryModule } = graphResult.value;
208
+
209
+ if (config.verbose) {
210
+ console.log(` Discovered ${modules.length} TypeScript modules`);
211
+ for (const module of modules) {
212
+ console.log(` - ${module.filePath}`);
213
+ }
214
+ }
215
+
216
+ // irResult.value was an array of modules, now it's graphResult.value.modules
217
+ const irResult = { ok: true as const, value: modules };
218
+
219
+ // Emit C# code
220
+ const absoluteEntryPoint = entryPoint ? resolve(entryPoint) : undefined;
221
+ const emitResult = emitCSharpFiles(irResult.value, {
222
+ rootNamespace,
223
+ entryPointPath: absoluteEntryPoint,
224
+ libraries: config.libraries,
225
+ runtime: config.runtime,
226
+ });
227
+
228
+ if (!emitResult.ok) {
229
+ // Handle file name collision errors
230
+ for (const error of emitResult.errors) {
231
+ console.error(`error ${error.code}: ${error.message}`);
232
+ }
233
+ process.exit(1);
234
+ }
235
+
236
+ const csFiles = emitResult.files;
237
+
238
+ // Create output directory
239
+ const outputDir = join(process.cwd(), outputDirectory);
240
+ mkdirSync(outputDir, { recursive: true });
241
+
242
+ // Write C# files preserving directory structure
243
+ for (const [modulePath, csCode] of csFiles) {
244
+ // Convert module path to C# file path
245
+ // src/models/User.ts → generated/src/models/User.cs
246
+ const csPath = modulePath.replace(/\.ts$/, ".cs");
247
+ const fullPath = join(outputDir, csPath);
248
+
249
+ mkdirSync(dirname(fullPath), { recursive: true });
250
+ writeFileSync(fullPath, csCode, "utf-8");
251
+
252
+ if (config.verbose) {
253
+ const relPath = relative(process.cwd(), fullPath);
254
+ console.log(` Generated: ${relPath}`);
255
+ }
256
+ }
257
+
258
+ // Generate Program.cs entry point wrapper (only for executables)
259
+ if (absoluteEntryPoint) {
260
+ // entryModule is already provided by buildDependencyGraph
261
+ // But double-check by comparing relative paths
262
+ const entryRelative = relative(sourceRoot, absoluteEntryPoint).replace(
263
+ /\\/g,
264
+ "/"
265
+ );
266
+ const foundEntryModule =
267
+ irResult.value.find((m: IrModule) => m.filePath === entryRelative) ??
268
+ entryModule;
269
+
270
+ if (foundEntryModule) {
271
+ const entryInfo = extractEntryInfo(foundEntryModule, config.runtime);
272
+ if (entryInfo) {
273
+ const programCs = generateProgramCs(entryInfo);
274
+ const programPath = join(outputDir, "Program.cs");
275
+ writeFileSync(programPath, programCs, "utf-8");
276
+
277
+ if (config.verbose) {
278
+ console.log(` Generated: ${relative(process.cwd(), programPath)}`);
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ // Generate or copy existing .csproj
285
+ const csprojPath = join(outputDir, "tsonic.csproj");
286
+ const projectCsproj = findProjectCsproj();
287
+
288
+ if (projectCsproj) {
289
+ // Copy existing .csproj from project root (preserves user edits)
290
+ copyFileSync(projectCsproj, csprojPath);
291
+
292
+ if (config.verbose) {
293
+ console.log(
294
+ ` Copied: ${relative(process.cwd(), projectCsproj)} → ${relative(process.cwd(), csprojPath)} (user edits preserved)`
295
+ );
296
+ }
297
+ } else if (!existsSync(csprojPath)) {
298
+ // Find Tsonic runtime - try multiple approaches:
299
+ // 1. ProjectReference to .csproj (development/monorepo)
300
+ // 2. Assembly references to DLLs (npm installed package)
301
+ let runtimePath: string | undefined;
302
+ let assemblyReferences: readonly AssemblyReference[] = [];
303
+
304
+ // 1. Try monorepo structure (development) - ProjectReference
305
+ const monorepoPath = resolve(
306
+ join(import.meta.dirname, "../../../runtime/src/Tsonic.Runtime.csproj")
307
+ );
308
+ if (existsSync(monorepoPath)) {
309
+ runtimePath = monorepoPath;
310
+ } else {
311
+ // 2. Try installed package structure - ProjectReference
312
+ const installedPath = resolve(
313
+ join(
314
+ import.meta.dirname,
315
+ "../../../../@tsonic/runtime/src/Tsonic.Runtime.csproj"
316
+ )
317
+ );
318
+ if (existsSync(installedPath)) {
319
+ runtimePath = installedPath;
320
+ } else {
321
+ // 3. Try to find runtime DLLs from npm package
322
+ assemblyReferences = findRuntimeDlls(
323
+ config.runtime ?? "js",
324
+ outputDir
325
+ );
326
+ }
327
+ }
328
+
329
+ // Warn if no runtime found
330
+ if (!runtimePath && assemblyReferences.length === 0 && !config.quiet) {
331
+ console.warn(
332
+ "Warning: Tsonic runtime not found. You may need to add references manually."
333
+ );
334
+ }
335
+
336
+ // Build output configuration
337
+ const outputType = config.outputConfig.type ?? "executable";
338
+ let outputConfig: ExecutableConfig | LibraryConfig;
339
+
340
+ if (outputType === "library") {
341
+ outputConfig = {
342
+ type: "library",
343
+ targetFrameworks: config.outputConfig.targetFrameworks ?? [
344
+ config.dotnetVersion,
345
+ ],
346
+ generateDocumentation:
347
+ config.outputConfig.generateDocumentation ?? true,
348
+ includeSymbols: config.outputConfig.includeSymbols ?? true,
349
+ packable: config.outputConfig.packable ?? false,
350
+ packageMetadata: config.outputConfig.package,
351
+ };
352
+ } else {
353
+ outputConfig = {
354
+ type: "executable",
355
+ nativeAot: config.outputConfig.nativeAot ?? true,
356
+ singleFile: config.outputConfig.singleFile ?? true,
357
+ trimmed: config.outputConfig.trimmed ?? true,
358
+ stripSymbols: config.stripSymbols,
359
+ optimization: config.optimize === "size" ? "Size" : "Speed",
360
+ invariantGlobalization: config.invariantGlobalization,
361
+ selfContained: config.outputConfig.selfContained ?? true,
362
+ };
363
+ }
364
+
365
+ const buildConfig: BuildConfig = {
366
+ rootNamespace,
367
+ outputName: config.outputName,
368
+ dotnetVersion: config.dotnetVersion,
369
+ runtimePath,
370
+ assemblyReferences,
371
+ packages,
372
+ outputConfig,
373
+ };
374
+
375
+ const csproj = generateCsproj(buildConfig);
376
+ writeFileSync(csprojPath, csproj, "utf-8");
377
+
378
+ if (config.verbose) {
379
+ console.log(` Generated: ${relative(process.cwd(), csprojPath)}`);
380
+ }
381
+ } else if (config.verbose) {
382
+ console.log(
383
+ ` Preserved: ${relative(process.cwd(), csprojPath)} (user edits kept)`
384
+ );
385
+ }
386
+
387
+ if (!config.quiet) {
388
+ console.log(
389
+ `\n✓ Generated ${csFiles.size} C# files in ${outputDirectory}/`
390
+ );
391
+ }
392
+
393
+ return {
394
+ ok: true,
395
+ value: {
396
+ filesGenerated: csFiles.size,
397
+ outputDir,
398
+ },
399
+ };
400
+ } catch (error) {
401
+ return {
402
+ ok: false,
403
+ error: `Emit failed: ${error instanceof Error ? error.message : String(error)}`,
404
+ };
405
+ }
406
+ };