@tsonic/frontend 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 (145) hide show
  1. package/package.json +53 -0
  2. package/src/dependency-graph.ts +18 -0
  3. package/src/dotnet-metadata.ts +121 -0
  4. package/src/graph/builder.ts +81 -0
  5. package/src/graph/circular.ts +58 -0
  6. package/src/graph/extraction/exports.ts +55 -0
  7. package/src/graph/extraction/imports.ts +81 -0
  8. package/src/graph/extraction/index.ts +7 -0
  9. package/src/graph/extraction/orchestrator.ts +99 -0
  10. package/src/graph/extraction.ts +10 -0
  11. package/src/graph/helpers.ts +51 -0
  12. package/src/graph/index.ts +17 -0
  13. package/src/graph/types.ts +13 -0
  14. package/src/index.ts +80 -0
  15. package/src/ir/binding-resolution.test.ts +585 -0
  16. package/src/ir/builder/exports.ts +78 -0
  17. package/src/ir/builder/helpers.ts +27 -0
  18. package/src/ir/builder/imports.ts +153 -0
  19. package/src/ir/builder/index.ts +10 -0
  20. package/src/ir/builder/orchestrator.ts +178 -0
  21. package/src/ir/builder/statements.ts +55 -0
  22. package/src/ir/builder/types.ts +8 -0
  23. package/src/ir/builder/validation.ts +129 -0
  24. package/src/ir/builder.test.ts +581 -0
  25. package/src/ir/builder.ts +14 -0
  26. package/src/ir/converters/expressions/access.ts +99 -0
  27. package/src/ir/converters/expressions/calls.ts +137 -0
  28. package/src/ir/converters/expressions/collections.ts +84 -0
  29. package/src/ir/converters/expressions/functions.ts +62 -0
  30. package/src/ir/converters/expressions/helpers.ts +264 -0
  31. package/src/ir/converters/expressions/index.ts +43 -0
  32. package/src/ir/converters/expressions/literals.ts +22 -0
  33. package/src/ir/converters/expressions/operators.ts +147 -0
  34. package/src/ir/converters/expressions/other.ts +60 -0
  35. package/src/ir/converters/statements/control/blocks.ts +22 -0
  36. package/src/ir/converters/statements/control/conditionals.ts +67 -0
  37. package/src/ir/converters/statements/control/exceptions.ts +43 -0
  38. package/src/ir/converters/statements/control/index.ts +17 -0
  39. package/src/ir/converters/statements/control/loops.ts +99 -0
  40. package/src/ir/converters/statements/control.ts +17 -0
  41. package/src/ir/converters/statements/declarations/classes/constructors.ts +120 -0
  42. package/src/ir/converters/statements/declarations/classes/index.ts +12 -0
  43. package/src/ir/converters/statements/declarations/classes/methods.ts +61 -0
  44. package/src/ir/converters/statements/declarations/classes/orchestrator.ts +166 -0
  45. package/src/ir/converters/statements/declarations/classes/override-detection.ts +116 -0
  46. package/src/ir/converters/statements/declarations/classes/properties.ts +63 -0
  47. package/src/ir/converters/statements/declarations/classes.ts +6 -0
  48. package/src/ir/converters/statements/declarations/enums.ts +29 -0
  49. package/src/ir/converters/statements/declarations/functions.ts +39 -0
  50. package/src/ir/converters/statements/declarations/index.ts +14 -0
  51. package/src/ir/converters/statements/declarations/interfaces.ts +131 -0
  52. package/src/ir/converters/statements/declarations/registry.ts +45 -0
  53. package/src/ir/converters/statements/declarations/type-aliases.ts +25 -0
  54. package/src/ir/converters/statements/declarations/variables.ts +60 -0
  55. package/src/ir/converters/statements/declarations.ts +16 -0
  56. package/src/ir/converters/statements/helpers.ts +174 -0
  57. package/src/ir/converters/statements/index.ts +40 -0
  58. package/src/ir/expression-converter.ts +207 -0
  59. package/src/ir/generic-validator.ts +100 -0
  60. package/src/ir/hierarchical-bindings-e2e.test.ts +163 -0
  61. package/src/ir/index.ts +6 -0
  62. package/src/ir/statement-converter.ts +128 -0
  63. package/src/ir/type-converter/arrays.ts +20 -0
  64. package/src/ir/type-converter/converter.ts +10 -0
  65. package/src/ir/type-converter/functions.ts +22 -0
  66. package/src/ir/type-converter/index.ts +11 -0
  67. package/src/ir/type-converter/inference.ts +122 -0
  68. package/src/ir/type-converter/literals.ts +40 -0
  69. package/src/ir/type-converter/objects.ts +107 -0
  70. package/src/ir/type-converter/orchestrator.ts +85 -0
  71. package/src/ir/type-converter/patterns.ts +73 -0
  72. package/src/ir/type-converter/primitives.ts +57 -0
  73. package/src/ir/type-converter/references.ts +64 -0
  74. package/src/ir/type-converter/unions-intersections.ts +34 -0
  75. package/src/ir/type-converter.ts +13 -0
  76. package/src/ir/types/expressions.ts +215 -0
  77. package/src/ir/types/guards.ts +39 -0
  78. package/src/ir/types/helpers.ts +135 -0
  79. package/src/ir/types/index.ts +108 -0
  80. package/src/ir/types/ir-types.ts +96 -0
  81. package/src/ir/types/module.ts +57 -0
  82. package/src/ir/types/statements.ts +238 -0
  83. package/src/ir/types.ts +97 -0
  84. package/src/metadata/bindings-loader.test.ts +144 -0
  85. package/src/metadata/bindings-loader.ts +357 -0
  86. package/src/metadata/index.ts +15 -0
  87. package/src/metadata/library-loader.ts +153 -0
  88. package/src/metadata/loader.test.ts +156 -0
  89. package/src/metadata/loader.ts +382 -0
  90. package/src/program/bindings.test.ts +512 -0
  91. package/src/program/bindings.ts +253 -0
  92. package/src/program/config.ts +30 -0
  93. package/src/program/creation.ts +249 -0
  94. package/src/program/dependency-graph.ts +245 -0
  95. package/src/program/diagnostics.ts +103 -0
  96. package/src/program/index.ts +19 -0
  97. package/src/program/metadata.ts +68 -0
  98. package/src/program/queries.ts +18 -0
  99. package/src/program/types.ts +38 -0
  100. package/src/program.ts +13 -0
  101. package/src/resolver/dotnet-import-resolver.ts +226 -0
  102. package/src/resolver/import-resolution.ts +177 -0
  103. package/src/resolver/index.ts +18 -0
  104. package/src/resolver/namespace.test.ts +86 -0
  105. package/src/resolver/namespace.ts +42 -0
  106. package/src/resolver/naming.ts +38 -0
  107. package/src/resolver/path-resolution.ts +22 -0
  108. package/src/resolver/types.ts +15 -0
  109. package/src/resolver.test.ts +155 -0
  110. package/src/resolver.ts +14 -0
  111. package/src/symbol-table/builder.ts +114 -0
  112. package/src/symbol-table/creation.ts +42 -0
  113. package/src/symbol-table/helpers.ts +18 -0
  114. package/src/symbol-table/index.ts +13 -0
  115. package/src/symbol-table/queries.ts +42 -0
  116. package/src/symbol-table/types.ts +28 -0
  117. package/src/symbol-table.ts +14 -0
  118. package/src/types/bindings.ts +172 -0
  119. package/src/types/diagnostic.test.ts +164 -0
  120. package/src/types/diagnostic.ts +153 -0
  121. package/src/types/explicit-views.test.ts +113 -0
  122. package/src/types/explicit-views.ts +218 -0
  123. package/src/types/metadata.ts +229 -0
  124. package/src/types/module.ts +99 -0
  125. package/src/types/nested-types.test.ts +194 -0
  126. package/src/types/nested-types.ts +215 -0
  127. package/src/types/parameter-modifiers.ts +173 -0
  128. package/src/types/ref-parameters.test.ts +192 -0
  129. package/src/types/ref-parameters.ts +268 -0
  130. package/src/types/result.test.ts +157 -0
  131. package/src/types/result.ts +48 -0
  132. package/src/types/support-types.test.ts +81 -0
  133. package/src/types/support-types.ts +288 -0
  134. package/src/types/test-harness.ts +180 -0
  135. package/src/validation/exports.ts +98 -0
  136. package/src/validation/features.ts +89 -0
  137. package/src/validation/generics.ts +40 -0
  138. package/src/validation/helpers.ts +31 -0
  139. package/src/validation/imports.ts +97 -0
  140. package/src/validation/index.ts +11 -0
  141. package/src/validation/orchestrator.ts +51 -0
  142. package/src/validation/static-safety.ts +267 -0
  143. package/src/validator.test.ts +468 -0
  144. package/src/validator.ts +15 -0
  145. package/tsconfig.json +13 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Import-driven .NET namespace resolution
3
+ *
4
+ * Determines if an import specifier refers to a CLR namespace by checking
5
+ * if bindings.json exists in the package's namespace directory.
6
+ *
7
+ * This is the ONLY mechanism for detecting .NET imports - no heuristics,
8
+ * no special-casing for @tsonic/dotnet or any other package.
9
+ */
10
+
11
+ import { existsSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { createRequire } from "node:module";
14
+
15
+ /**
16
+ * Result of resolving a .NET import
17
+ */
18
+ export type ResolvedDotNetImport =
19
+ | {
20
+ readonly isDotNet: true;
21
+ readonly packageName: string;
22
+ readonly resolvedNamespace: string;
23
+ readonly bindingsPath: string;
24
+ readonly metadataPath: string | undefined;
25
+ }
26
+ | {
27
+ readonly isDotNet: false;
28
+ };
29
+
30
+ /**
31
+ * Parsed module specifier
32
+ */
33
+ type ParsedSpecifier = {
34
+ readonly packageName: string;
35
+ readonly subpath: string;
36
+ };
37
+
38
+ /**
39
+ * Resolver for .NET namespace imports
40
+ *
41
+ * An import is a CLR namespace import iff:
42
+ * - It's a package import with a subpath (e.g., @scope/pkg/Namespace)
43
+ * - The package contains bindings.json in that subpath directory
44
+ *
45
+ * This works for any bindings provider package, not just @tsonic/dotnet.
46
+ */
47
+ export class DotNetImportResolver {
48
+ // Cache: packageName -> packageRoot (or null if not found)
49
+ private readonly pkgRootCache = new Map<string, string | null>();
50
+
51
+ // Cache: absolute bindingsPath -> exists
52
+ private readonly bindingsExistsCache = new Map<string, boolean>();
53
+
54
+ // require function for Node resolution from the base directory
55
+ private readonly require: NodeRequire;
56
+
57
+ constructor(baseDir: string) {
58
+ // Create a require function that resolves relative to baseDir
59
+ this.require = createRequire(join(baseDir, "package.json"));
60
+ }
61
+
62
+ /**
63
+ * Resolve an import specifier to determine if it's a CLR namespace import
64
+ */
65
+ resolve(moduleSpecifier: string): ResolvedDotNetImport {
66
+ // Skip local imports
67
+ if (moduleSpecifier.startsWith(".") || moduleSpecifier.startsWith("/")) {
68
+ return { isDotNet: false };
69
+ }
70
+
71
+ // Parse the specifier into package name and subpath
72
+ const parsed = this.parseModuleSpecifier(moduleSpecifier);
73
+ if (!parsed) {
74
+ return { isDotNet: false };
75
+ }
76
+
77
+ const { packageName, subpath } = parsed;
78
+
79
+ // No subpath means it's not a namespace import (e.g., just "@tsonic/dotnet")
80
+ if (!subpath) {
81
+ return { isDotNet: false };
82
+ }
83
+
84
+ // Resolve package root using Node resolution
85
+ const pkgRoot = this.resolvePkgRoot(packageName);
86
+ if (!pkgRoot) {
87
+ return { isDotNet: false };
88
+ }
89
+
90
+ // Check if bindings.json exists in the namespace directory
91
+ const bindingsPath = join(pkgRoot, subpath, "bindings.json");
92
+ if (!this.hasBindings(bindingsPath)) {
93
+ return { isDotNet: false };
94
+ }
95
+
96
+ // Check for optional metadata.json
97
+ const metadataPath = join(pkgRoot, subpath, "internal", "metadata.json");
98
+ const hasMetadata = this.fileExists(metadataPath);
99
+
100
+ return {
101
+ isDotNet: true,
102
+ packageName,
103
+ resolvedNamespace: subpath,
104
+ bindingsPath,
105
+ metadataPath: hasMetadata ? metadataPath : undefined,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Parse a module specifier into package name and subpath
111
+ *
112
+ * Examples:
113
+ * - "@tsonic/dotnet/System.IO" -> { packageName: "@tsonic/dotnet", subpath: "System.IO" }
114
+ * - "lodash/fp" -> { packageName: "lodash", subpath: "fp" }
115
+ * - "@tsonic/dotnet" -> null (no subpath)
116
+ * - "lodash" -> null (no subpath)
117
+ */
118
+ private parseModuleSpecifier(spec: string): ParsedSpecifier | null {
119
+ if (spec.startsWith("@")) {
120
+ // Scoped package: @scope/pkg/subpath
121
+ const match = spec.match(/^(@[^/]+\/[^/]+)(?:\/(.+))?$/);
122
+ if (!match) return null;
123
+ const packageName = match[1];
124
+ const subpath = match[2];
125
+ if (!packageName || !subpath) return null;
126
+ return { packageName, subpath };
127
+ } else {
128
+ // Regular package: pkg/subpath
129
+ const slashIdx = spec.indexOf("/");
130
+ if (slashIdx === -1) return null;
131
+ return {
132
+ packageName: spec.slice(0, slashIdx),
133
+ subpath: spec.slice(slashIdx + 1),
134
+ };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Resolve package root directory using Node resolution
140
+ *
141
+ * Uses require.resolve to find the package's package.json,
142
+ * then returns the directory containing it.
143
+ */
144
+ private resolvePkgRoot(packageName: string): string | null {
145
+ const cached = this.pkgRootCache.get(packageName);
146
+ if (cached !== undefined) {
147
+ return cached;
148
+ }
149
+
150
+ try {
151
+ // Try to resolve the package's package.json directly
152
+ const pkgJsonPath = this.require.resolve(`${packageName}/package.json`);
153
+ const pkgRoot = dirname(pkgJsonPath);
154
+ this.pkgRootCache.set(packageName, pkgRoot);
155
+ return pkgRoot;
156
+ } catch {
157
+ // package.json might not be exported - try finding package root via node_modules path
158
+ // This works by looking for node_modules/<package-name> in the require paths
159
+ try {
160
+ const paths = this.require.resolve.paths(packageName);
161
+ if (paths) {
162
+ for (const searchPath of paths) {
163
+ // For scoped packages, the path structure is node_modules/@scope/name
164
+ const pkgDir = packageName.startsWith("@")
165
+ ? join(searchPath, packageName)
166
+ : join(searchPath, packageName);
167
+
168
+ // Check if package.json exists at this location
169
+ if (existsSync(join(pkgDir, "package.json"))) {
170
+ this.pkgRootCache.set(packageName, pkgDir);
171
+ return pkgDir;
172
+ }
173
+ }
174
+ }
175
+ } catch {
176
+ // Fallback failed
177
+ }
178
+
179
+ this.pkgRootCache.set(packageName, null);
180
+ return null;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Check if bindings.json exists at the given path (cached)
186
+ */
187
+ private hasBindings(bindingsPath: string): boolean {
188
+ const cached = this.bindingsExistsCache.get(bindingsPath);
189
+ if (cached !== undefined) {
190
+ return cached;
191
+ }
192
+
193
+ const exists = existsSync(bindingsPath);
194
+ this.bindingsExistsCache.set(bindingsPath, exists);
195
+ return exists;
196
+ }
197
+
198
+ /**
199
+ * Check if a file exists (not cached - used for optional files)
200
+ */
201
+ private fileExists(filePath: string): boolean {
202
+ return existsSync(filePath);
203
+ }
204
+
205
+ /**
206
+ * Get all discovered binding paths (for loading)
207
+ */
208
+ getDiscoveredBindingPaths(): ReadonlySet<string> {
209
+ const paths = new Set<string>();
210
+ for (const [path, exists] of this.bindingsExistsCache) {
211
+ if (exists) {
212
+ paths.add(path);
213
+ }
214
+ }
215
+ return paths;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Create a resolver instance for a given source root
221
+ */
222
+ export const createDotNetImportResolver = (
223
+ sourceRoot: string
224
+ ): DotNetImportResolver => {
225
+ return new DotNetImportResolver(sourceRoot);
226
+ };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Import resolution with ESM rules enforcement
3
+ */
4
+
5
+ import * as path from "node:path";
6
+ import * as fs from "node:fs";
7
+ import { Result, ok, error } from "../types/result.js";
8
+ import { Diagnostic, createDiagnostic } from "../types/diagnostic.js";
9
+ import { isLocalImport } from "../types/module.js";
10
+ import { ResolvedModule } from "./types.js";
11
+ import { getBindingRegistry } from "../ir/converters/statements/declarations/registry.js";
12
+ import { DotNetImportResolver } from "./dotnet-import-resolver.js";
13
+
14
+ /**
15
+ * Resolve import specifier to module
16
+ */
17
+ export const resolveImport = (
18
+ importSpecifier: string,
19
+ containingFile: string,
20
+ sourceRoot: string,
21
+ dotnetResolver?: DotNetImportResolver
22
+ ): Result<ResolvedModule, Diagnostic> => {
23
+ if (isLocalImport(importSpecifier)) {
24
+ return resolveLocalImport(importSpecifier, containingFile, sourceRoot);
25
+ }
26
+
27
+ // Use import-driven resolution for .NET imports (if resolver provided)
28
+ if (dotnetResolver) {
29
+ const dotnetResolution = dotnetResolver.resolve(importSpecifier);
30
+ if (dotnetResolution.isDotNet) {
31
+ return ok({
32
+ resolvedPath: "", // No file path for .NET imports
33
+ isLocal: false,
34
+ isDotNet: true,
35
+ originalSpecifier: importSpecifier,
36
+ resolvedNamespace: dotnetResolution.resolvedNamespace,
37
+ });
38
+ }
39
+ }
40
+
41
+ // Check if this is a module binding (e.g., Node.js API)
42
+ const binding = getBindingRegistry().getBinding(importSpecifier);
43
+ if (binding && binding.kind === "module") {
44
+ return ok({
45
+ resolvedPath: "", // No file path for bound modules
46
+ isLocal: false,
47
+ isDotNet: false,
48
+ originalSpecifier: importSpecifier,
49
+ resolvedClrType: binding.type,
50
+ resolvedAssembly: binding.assembly,
51
+ });
52
+ }
53
+
54
+ // @tsonic/types is a type-only package (phantom types) - no runtime code
55
+ if (importSpecifier === "@tsonic/types") {
56
+ return ok({
57
+ resolvedPath: "", // No file path for type-only packages
58
+ isLocal: false,
59
+ isDotNet: false,
60
+ originalSpecifier: importSpecifier,
61
+ resolvedClrType: undefined,
62
+ resolvedAssembly: undefined,
63
+ });
64
+ }
65
+
66
+ return error(
67
+ createDiagnostic(
68
+ "TSN1004",
69
+ "error",
70
+ `Unsupported module import: "${importSpecifier}"`,
71
+ undefined,
72
+ "Tsonic only supports local imports (with .ts), .NET imports, and registered module bindings"
73
+ )
74
+ );
75
+ };
76
+
77
+ /**
78
+ * Resolve local import with ESM rules
79
+ */
80
+ export const resolveLocalImport = (
81
+ importSpecifier: string,
82
+ containingFile: string,
83
+ sourceRoot: string
84
+ ): Result<ResolvedModule, Diagnostic> => {
85
+ // Check for .ts extension
86
+ if (!importSpecifier.endsWith(".ts")) {
87
+ return error(
88
+ createDiagnostic(
89
+ "TSN1001",
90
+ "error",
91
+ `Local import must have .ts extension: "${importSpecifier}"`,
92
+ undefined,
93
+ `Change to: "${importSpecifier}.ts"`
94
+ )
95
+ );
96
+ }
97
+
98
+ const containingDir = path.dirname(containingFile);
99
+ const resolvedPath = path.resolve(containingDir, importSpecifier);
100
+
101
+ // Check if file exists
102
+ if (!fs.existsSync(resolvedPath)) {
103
+ return error(
104
+ createDiagnostic(
105
+ "TSN1004",
106
+ "error",
107
+ `Cannot find module: "${importSpecifier}"`,
108
+ undefined,
109
+ `File not found: ${resolvedPath}`
110
+ )
111
+ );
112
+ }
113
+
114
+ // Check case sensitivity
115
+ const realPath = fs.realpathSync(resolvedPath);
116
+ if (realPath !== resolvedPath && process.platform !== "win32") {
117
+ return error(
118
+ createDiagnostic(
119
+ "TSN1003",
120
+ "error",
121
+ `Case mismatch in import path: "${importSpecifier}"`,
122
+ undefined,
123
+ `File exists as: ${realPath}`
124
+ )
125
+ );
126
+ }
127
+
128
+ // Ensure it's within the source root
129
+ if (!resolvedPath.startsWith(sourceRoot)) {
130
+ return error(
131
+ createDiagnostic(
132
+ "TSN1004",
133
+ "error",
134
+ `Import outside source root: "${importSpecifier}"`,
135
+ undefined,
136
+ `Source root: ${sourceRoot}`
137
+ )
138
+ );
139
+ }
140
+
141
+ return ok({
142
+ resolvedPath,
143
+ isLocal: true,
144
+ isDotNet: false,
145
+ originalSpecifier: importSpecifier,
146
+ });
147
+ };
148
+
149
+ /**
150
+ * Resolve .NET import (namespace validation)
151
+ */
152
+ export const resolveDotNetImport = (
153
+ importSpecifier: string
154
+ ): Result<ResolvedModule, Diagnostic> => {
155
+ // For .NET imports, we don't resolve to a file
156
+ // We just validate the format and return the namespace
157
+
158
+ // Check for invalid characters
159
+ if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(importSpecifier)) {
160
+ return error(
161
+ createDiagnostic(
162
+ "TSN4001",
163
+ "error",
164
+ `Invalid .NET namespace: "${importSpecifier}"`,
165
+ undefined,
166
+ "Must be a valid .NET namespace identifier"
167
+ )
168
+ );
169
+ }
170
+
171
+ return ok({
172
+ resolvedPath: "", // No file path for .NET imports
173
+ isLocal: false,
174
+ isDotNet: true,
175
+ originalSpecifier: importSpecifier,
176
+ });
177
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Module resolver - Public API
3
+ */
4
+
5
+ export type { ResolvedModule } from "./types.js";
6
+ export {
7
+ resolveImport,
8
+ resolveLocalImport,
9
+ resolveDotNetImport,
10
+ } from "./import-resolution.js";
11
+ export { resolveModulePath } from "./path-resolution.js";
12
+ export { getNamespaceFromPath } from "./namespace.js";
13
+ export { getClassNameFromPath } from "./naming.js";
14
+ export {
15
+ DotNetImportResolver,
16
+ createDotNetImportResolver,
17
+ type ResolvedDotNetImport,
18
+ } from "./dotnet-import-resolver.js";
@@ -0,0 +1,86 @@
1
+ import { describe, it } from "mocha";
2
+ import { expect } from "chai";
3
+ import { getNamespaceFromPath } from "./namespace.js";
4
+
5
+ describe("getNamespaceFromPath", () => {
6
+ it("should return root namespace for files directly in source root", () => {
7
+ const result = getNamespaceFromPath(
8
+ "/project/src/index.ts",
9
+ "/project/src",
10
+ "MyApp"
11
+ );
12
+ expect(result).to.equal("MyApp");
13
+ });
14
+
15
+ it("should return root namespace for files in source root without src directory", () => {
16
+ const result = getNamespaceFromPath(
17
+ "/project/index.ts",
18
+ "/project",
19
+ "MyApp"
20
+ );
21
+ expect(result).to.equal("MyApp");
22
+ });
23
+
24
+ it("should compute namespace from single subdirectory", () => {
25
+ const result = getNamespaceFromPath(
26
+ "/project/src/models/user.ts",
27
+ "/project/src",
28
+ "MyApp"
29
+ );
30
+ expect(result).to.equal("MyApp.models");
31
+ });
32
+
33
+ it("should compute namespace from nested subdirectories", () => {
34
+ const result = getNamespaceFromPath(
35
+ "/project/src/models/entities/user.ts",
36
+ "/project/src",
37
+ "MyApp"
38
+ );
39
+ expect(result).to.equal("MyApp.models.entities");
40
+ });
41
+
42
+ it("should filter out 'src' from path components", () => {
43
+ const result = getNamespaceFromPath(
44
+ "/project/src/models/src/user.ts",
45
+ "/project/src",
46
+ "MyApp"
47
+ );
48
+ expect(result).to.equal("MyApp.models");
49
+ });
50
+
51
+ it("should handle case-preserved directory names", () => {
52
+ const result = getNamespaceFromPath(
53
+ "/project/src/MyModels/UserEntity.ts",
54
+ "/project/src",
55
+ "MyApp"
56
+ );
57
+ expect(result).to.equal("MyApp.MyModels");
58
+ });
59
+
60
+ it("should handle deep nesting", () => {
61
+ const result = getNamespaceFromPath(
62
+ "/project/src/a/b/c/d/e/file.ts",
63
+ "/project/src",
64
+ "MyApp"
65
+ );
66
+ expect(result).to.equal("MyApp.a.b.c.d.e");
67
+ });
68
+
69
+ it("should handle source root without trailing slash", () => {
70
+ const result = getNamespaceFromPath(
71
+ "/project/src/models/user.ts",
72
+ "/project/src",
73
+ "MyApp"
74
+ );
75
+ expect(result).to.equal("MyApp.models");
76
+ });
77
+
78
+ it("should handle source root with trailing slash", () => {
79
+ const result = getNamespaceFromPath(
80
+ "/project/src/models/user.ts",
81
+ "/project/src/",
82
+ "MyApp"
83
+ );
84
+ expect(result).to.equal("MyApp.models");
85
+ });
86
+ });
@@ -0,0 +1,42 @@
1
+ import { dirname, relative } from "path";
2
+
3
+ /**
4
+ * Compute namespace from file path relative to source root.
5
+ *
6
+ * Rules:
7
+ * - Namespace = rootNamespace + path components (excluding "src")
8
+ * - Path normalized with "/" separators after relative() call
9
+ * - Empty path maps to root namespace
10
+ *
11
+ * Examples:
12
+ * - /project/src/models/user.ts → RootNamespace.models
13
+ * - /project/src/index.ts → RootNamespace
14
+ * - /project/models/user.ts → RootNamespace.models
15
+ *
16
+ * @param filePath - Absolute path to the TypeScript file
17
+ * @param sourceRoot - Absolute path to source root directory
18
+ * @param rootNamespace - Root namespace for the project
19
+ * @returns Computed namespace string
20
+ */
21
+ export const getNamespaceFromPath = (
22
+ filePath: string,
23
+ sourceRoot: string,
24
+ rootNamespace: string
25
+ ): string => {
26
+ const fileDir = dirname(filePath);
27
+
28
+ // Make relative to source root, then normalize to forward slashes
29
+ // IMPORTANT: Normalize AFTER relative() call, not before (cross-platform)
30
+ let relativePath = relative(sourceRoot, fileDir).replace(/\\/g, "/");
31
+
32
+ // Split by "/" and filter out empty, ".", and "src" components
33
+ const parts = relativePath
34
+ .split("/")
35
+ .filter((p) => p !== "" && p !== "." && p !== "src");
36
+
37
+ // If no path components, return root namespace
38
+ // Otherwise, join with "."
39
+ return parts.length === 0
40
+ ? rootNamespace
41
+ : `${rootNamespace}.${parts.join(".")}`;
42
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Namespace and class name generation from file paths
3
+ */
4
+
5
+ import * as path from "node:path";
6
+ import { resolveModulePath } from "./path-resolution.js";
7
+
8
+ /**
9
+ * Generate namespace from file path
10
+ */
11
+ export const getNamespaceFromPath = (
12
+ filePath: string,
13
+ sourceRoot: string,
14
+ rootNamespace: string
15
+ ): string => {
16
+ const relativePath = resolveModulePath(filePath, sourceRoot);
17
+ const dirPath = path.dirname(relativePath);
18
+
19
+ if (dirPath === ".") {
20
+ return rootNamespace;
21
+ }
22
+
23
+ // Strip hyphens from directory names (per spec: hyphens are ignored)
24
+ const parts = dirPath
25
+ .split(path.sep)
26
+ .filter((p) => p !== ".")
27
+ .map((p) => p.replace(/-/g, ""));
28
+ return [rootNamespace, ...parts].join(".");
29
+ };
30
+
31
+ /**
32
+ * Generate class name from file path
33
+ */
34
+ export const getClassNameFromPath = (filePath: string): string => {
35
+ const basename = path.basename(filePath, ".ts");
36
+ // Strip hyphens from file names (per spec: hyphens are ignored)
37
+ return basename.replace(/-/g, "");
38
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Path resolution utilities
3
+ */
4
+
5
+ import * as path from "node:path";
6
+
7
+ /**
8
+ * Resolve module path relative to source root
9
+ */
10
+ export const resolveModulePath = (
11
+ filePath: string,
12
+ sourceRoot: string
13
+ ): string => {
14
+ const absolutePath = path.resolve(filePath);
15
+ const absoluteRoot = path.resolve(sourceRoot);
16
+
17
+ if (!absolutePath.startsWith(absoluteRoot)) {
18
+ throw new Error(`File ${filePath} is outside source root ${sourceRoot}`);
19
+ }
20
+
21
+ return path.relative(absoluteRoot, absolutePath);
22
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resolver type definitions
3
+ */
4
+
5
+ export type ResolvedModule = {
6
+ readonly resolvedPath: string;
7
+ readonly isLocal: boolean;
8
+ readonly isDotNet: boolean;
9
+ readonly originalSpecifier: string;
10
+ // For .NET imports: the CLR namespace (e.g., "System" from "@tsonic/dotnet/System")
11
+ readonly resolvedNamespace?: string;
12
+ // For module bindings (Node.js APIs mapped to CLR types)
13
+ readonly resolvedClrType?: string; // e.g., "Tsonic.NodeApi.fs"
14
+ readonly resolvedAssembly?: string; // e.g., "Tsonic.NodeApi"
15
+ };