@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.
- package/package.json +53 -0
- package/src/dependency-graph.ts +18 -0
- package/src/dotnet-metadata.ts +121 -0
- package/src/graph/builder.ts +81 -0
- package/src/graph/circular.ts +58 -0
- package/src/graph/extraction/exports.ts +55 -0
- package/src/graph/extraction/imports.ts +81 -0
- package/src/graph/extraction/index.ts +7 -0
- package/src/graph/extraction/orchestrator.ts +99 -0
- package/src/graph/extraction.ts +10 -0
- package/src/graph/helpers.ts +51 -0
- package/src/graph/index.ts +17 -0
- package/src/graph/types.ts +13 -0
- package/src/index.ts +80 -0
- package/src/ir/binding-resolution.test.ts +585 -0
- package/src/ir/builder/exports.ts +78 -0
- package/src/ir/builder/helpers.ts +27 -0
- package/src/ir/builder/imports.ts +153 -0
- package/src/ir/builder/index.ts +10 -0
- package/src/ir/builder/orchestrator.ts +178 -0
- package/src/ir/builder/statements.ts +55 -0
- package/src/ir/builder/types.ts +8 -0
- package/src/ir/builder/validation.ts +129 -0
- package/src/ir/builder.test.ts +581 -0
- package/src/ir/builder.ts +14 -0
- package/src/ir/converters/expressions/access.ts +99 -0
- package/src/ir/converters/expressions/calls.ts +137 -0
- package/src/ir/converters/expressions/collections.ts +84 -0
- package/src/ir/converters/expressions/functions.ts +62 -0
- package/src/ir/converters/expressions/helpers.ts +264 -0
- package/src/ir/converters/expressions/index.ts +43 -0
- package/src/ir/converters/expressions/literals.ts +22 -0
- package/src/ir/converters/expressions/operators.ts +147 -0
- package/src/ir/converters/expressions/other.ts +60 -0
- package/src/ir/converters/statements/control/blocks.ts +22 -0
- package/src/ir/converters/statements/control/conditionals.ts +67 -0
- package/src/ir/converters/statements/control/exceptions.ts +43 -0
- package/src/ir/converters/statements/control/index.ts +17 -0
- package/src/ir/converters/statements/control/loops.ts +99 -0
- package/src/ir/converters/statements/control.ts +17 -0
- package/src/ir/converters/statements/declarations/classes/constructors.ts +120 -0
- package/src/ir/converters/statements/declarations/classes/index.ts +12 -0
- package/src/ir/converters/statements/declarations/classes/methods.ts +61 -0
- package/src/ir/converters/statements/declarations/classes/orchestrator.ts +166 -0
- package/src/ir/converters/statements/declarations/classes/override-detection.ts +116 -0
- package/src/ir/converters/statements/declarations/classes/properties.ts +63 -0
- package/src/ir/converters/statements/declarations/classes.ts +6 -0
- package/src/ir/converters/statements/declarations/enums.ts +29 -0
- package/src/ir/converters/statements/declarations/functions.ts +39 -0
- package/src/ir/converters/statements/declarations/index.ts +14 -0
- package/src/ir/converters/statements/declarations/interfaces.ts +131 -0
- package/src/ir/converters/statements/declarations/registry.ts +45 -0
- package/src/ir/converters/statements/declarations/type-aliases.ts +25 -0
- package/src/ir/converters/statements/declarations/variables.ts +60 -0
- package/src/ir/converters/statements/declarations.ts +16 -0
- package/src/ir/converters/statements/helpers.ts +174 -0
- package/src/ir/converters/statements/index.ts +40 -0
- package/src/ir/expression-converter.ts +207 -0
- package/src/ir/generic-validator.ts +100 -0
- package/src/ir/hierarchical-bindings-e2e.test.ts +163 -0
- package/src/ir/index.ts +6 -0
- package/src/ir/statement-converter.ts +128 -0
- package/src/ir/type-converter/arrays.ts +20 -0
- package/src/ir/type-converter/converter.ts +10 -0
- package/src/ir/type-converter/functions.ts +22 -0
- package/src/ir/type-converter/index.ts +11 -0
- package/src/ir/type-converter/inference.ts +122 -0
- package/src/ir/type-converter/literals.ts +40 -0
- package/src/ir/type-converter/objects.ts +107 -0
- package/src/ir/type-converter/orchestrator.ts +85 -0
- package/src/ir/type-converter/patterns.ts +73 -0
- package/src/ir/type-converter/primitives.ts +57 -0
- package/src/ir/type-converter/references.ts +64 -0
- package/src/ir/type-converter/unions-intersections.ts +34 -0
- package/src/ir/type-converter.ts +13 -0
- package/src/ir/types/expressions.ts +215 -0
- package/src/ir/types/guards.ts +39 -0
- package/src/ir/types/helpers.ts +135 -0
- package/src/ir/types/index.ts +108 -0
- package/src/ir/types/ir-types.ts +96 -0
- package/src/ir/types/module.ts +57 -0
- package/src/ir/types/statements.ts +238 -0
- package/src/ir/types.ts +97 -0
- package/src/metadata/bindings-loader.test.ts +144 -0
- package/src/metadata/bindings-loader.ts +357 -0
- package/src/metadata/index.ts +15 -0
- package/src/metadata/library-loader.ts +153 -0
- package/src/metadata/loader.test.ts +156 -0
- package/src/metadata/loader.ts +382 -0
- package/src/program/bindings.test.ts +512 -0
- package/src/program/bindings.ts +253 -0
- package/src/program/config.ts +30 -0
- package/src/program/creation.ts +249 -0
- package/src/program/dependency-graph.ts +245 -0
- package/src/program/diagnostics.ts +103 -0
- package/src/program/index.ts +19 -0
- package/src/program/metadata.ts +68 -0
- package/src/program/queries.ts +18 -0
- package/src/program/types.ts +38 -0
- package/src/program.ts +13 -0
- package/src/resolver/dotnet-import-resolver.ts +226 -0
- package/src/resolver/import-resolution.ts +177 -0
- package/src/resolver/index.ts +18 -0
- package/src/resolver/namespace.test.ts +86 -0
- package/src/resolver/namespace.ts +42 -0
- package/src/resolver/naming.ts +38 -0
- package/src/resolver/path-resolution.ts +22 -0
- package/src/resolver/types.ts +15 -0
- package/src/resolver.test.ts +155 -0
- package/src/resolver.ts +14 -0
- package/src/symbol-table/builder.ts +114 -0
- package/src/symbol-table/creation.ts +42 -0
- package/src/symbol-table/helpers.ts +18 -0
- package/src/symbol-table/index.ts +13 -0
- package/src/symbol-table/queries.ts +42 -0
- package/src/symbol-table/types.ts +28 -0
- package/src/symbol-table.ts +14 -0
- package/src/types/bindings.ts +172 -0
- package/src/types/diagnostic.test.ts +164 -0
- package/src/types/diagnostic.ts +153 -0
- package/src/types/explicit-views.test.ts +113 -0
- package/src/types/explicit-views.ts +218 -0
- package/src/types/metadata.ts +229 -0
- package/src/types/module.ts +99 -0
- package/src/types/nested-types.test.ts +194 -0
- package/src/types/nested-types.ts +215 -0
- package/src/types/parameter-modifiers.ts +173 -0
- package/src/types/ref-parameters.test.ts +192 -0
- package/src/types/ref-parameters.ts +268 -0
- package/src/types/result.test.ts +157 -0
- package/src/types/result.ts +48 -0
- package/src/types/support-types.test.ts +81 -0
- package/src/types/support-types.ts +288 -0
- package/src/types/test-harness.ts +180 -0
- package/src/validation/exports.ts +98 -0
- package/src/validation/features.ts +89 -0
- package/src/validation/generics.ts +40 -0
- package/src/validation/helpers.ts +31 -0
- package/src/validation/imports.ts +97 -0
- package/src/validation/index.ts +11 -0
- package/src/validation/orchestrator.ts +51 -0
- package/src/validation/static-safety.ts +267 -0
- package/src/validator.test.ts +468 -0
- package/src/validator.ts +15 -0
- 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
|
+
};
|