@tsvm/ts-semantics 0.1.0

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.
@@ -0,0 +1,19 @@
1
+ import { ProjectSemanticGraph, TypeFact, ModuleInfo, DependencyEdge, SymbolAlias } from '@tsvm/shared';
2
+ import ts from 'typescript';
3
+
4
+ declare function analyzeProject(tsconfigPath: string, entryPoints?: readonly string[]): ProjectSemanticGraph;
5
+
6
+ declare function extractTypeFacts(sourceFile: ts.SourceFile, checker: ts.TypeChecker): TypeFact[];
7
+
8
+ declare function buildModuleGraph(program: ts.Program, checker: ts.TypeChecker): {
9
+ modules: Map<string, ModuleInfo>;
10
+ dependencyEdges: DependencyEdge[];
11
+ entries: string[];
12
+ };
13
+
14
+ declare function buildSymbolTable(modules: Map<string, ModuleInfo>): {
15
+ symbolTable: TypeFact[];
16
+ aliases: Map<number, SymbolAlias>;
17
+ };
18
+
19
+ export { analyzeProject, buildModuleGraph, buildSymbolTable, extractTypeFacts };
package/dist/index.js ADDED
@@ -0,0 +1,344 @@
1
+ // src/project.ts
2
+ import ts3 from "typescript";
3
+ import { DiagnosticSeverity } from "@tsvm/shared";
4
+
5
+ // src/module-graph.ts
6
+ import ts2 from "typescript";
7
+ import { TypeFactKind as TypeFactKind2 } from "@tsvm/shared";
8
+
9
+ // src/type-facts.ts
10
+ import ts from "typescript";
11
+ import { TypeFactKind } from "@tsvm/shared";
12
+ function getSourceLocation(node) {
13
+ const sourceFile = node.getSourceFile();
14
+ const start = node.getStart();
15
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
16
+ return {
17
+ filePath: sourceFile.fileName,
18
+ line,
19
+ column: character,
20
+ offset: start,
21
+ length: node.getEnd() - start
22
+ };
23
+ }
24
+ function extractTypeFacts(sourceFile, checker) {
25
+ const facts = [];
26
+ function visit(node) {
27
+ if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node) || ts.isParameter(node) || ts.isPropertyDeclaration(node) || ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isConstructorDeclaration(node)) {
28
+ if (node.name && ts.isIdentifier(node.name)) {
29
+ const symbol = checker.getSymbolAtLocation(node.name);
30
+ if (symbol) {
31
+ const type = checker.getTypeOfSymbolAtLocation(symbol, node);
32
+ const typeText = checker.typeToString(type, node, ts.TypeFormatFlags.NoTruncation);
33
+ let kind = TypeFactKind.Variable;
34
+ if (ts.isFunctionDeclaration(node)) kind = TypeFactKind.Function;
35
+ else if (ts.isClassDeclaration(node)) kind = TypeFactKind.Class;
36
+ else if (ts.isInterfaceDeclaration(node)) kind = TypeFactKind.Interface;
37
+ else if (ts.isTypeAliasDeclaration(node)) kind = TypeFactKind.TypeAlias;
38
+ else if (ts.isEnumDeclaration(node)) kind = TypeFactKind.Enum;
39
+ else if (ts.isModuleDeclaration(node)) kind = TypeFactKind.Namespace;
40
+ else if (ts.isParameter(node)) kind = TypeFactKind.Parameter;
41
+ else if (ts.isPropertyDeclaration(node)) kind = TypeFactKind.Property;
42
+ else if (ts.isMethodDeclaration(node)) kind = TypeFactKind.Method;
43
+ else if (ts.isGetAccessor(node) || ts.isSetAccessor(node)) kind = TypeFactKind.Accessor;
44
+ else if (ts.isConstructorDeclaration(node)) kind = TypeFactKind.Constructor;
45
+ let isExported = false;
46
+ let isAmbient = false;
47
+ if (ts.canHaveModifiers(node)) {
48
+ const modifiers = ts.getModifiers(node);
49
+ isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
50
+ isAmbient = modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) ?? false;
51
+ }
52
+ let isGeneric = false;
53
+ const typeParameters = [];
54
+ const constraints = [];
55
+ if ("typeParameters" in node) {
56
+ const nodeWithTP = node;
57
+ const tps = nodeWithTP.typeParameters;
58
+ if (tps) {
59
+ isGeneric = true;
60
+ tps.forEach((tp) => {
61
+ typeParameters.push(tp.name.text);
62
+ if (tp.constraint) {
63
+ constraints.push(tp.constraint.getText());
64
+ }
65
+ });
66
+ }
67
+ }
68
+ const decorators = [];
69
+ if (ts.canHaveDecorators(node)) {
70
+ const decs = ts.getDecorators(node);
71
+ if (decs) {
72
+ decs.forEach((d) => decorators.push(d.expression.getText()));
73
+ }
74
+ }
75
+ facts.push({
76
+ symbolName: symbol.name,
77
+ symbolId: symbol.id ?? -1,
78
+ kind,
79
+ typeText,
80
+ flags: type.flags,
81
+ isGeneric,
82
+ typeParameters,
83
+ constraints,
84
+ sourceLocation: getSourceLocation(node),
85
+ isExported,
86
+ isAmbient,
87
+ decorators
88
+ });
89
+ }
90
+ }
91
+ }
92
+ ts.forEachChild(node, visit);
93
+ }
94
+ visit(sourceFile);
95
+ return facts;
96
+ }
97
+
98
+ // src/module-graph.ts
99
+ function buildModuleGraph(program, checker) {
100
+ const modules = /* @__PURE__ */ new Map();
101
+ const dependencyEdges = [];
102
+ const entries = [];
103
+ const sourceFiles = program.getSourceFiles().filter((sf) => !sf.isDeclarationFile && !sf.fileName.includes("node_modules"));
104
+ for (const sf of sourceFiles) {
105
+ const filePath = sf.fileName;
106
+ const typeFacts = extractTypeFacts(sf, checker);
107
+ const exports = [];
108
+ const imports = [];
109
+ let hasJSX = false;
110
+ let hasDecorators = false;
111
+ ts2.forEachChild(sf, function visit(node) {
112
+ if (ts2.isJsxElement(node) || ts2.isJsxSelfClosingElement(node) || ts2.isJsxFragment(node)) {
113
+ hasJSX = true;
114
+ }
115
+ if (ts2.canHaveDecorators(node) && ts2.getDecorators(node)?.length) {
116
+ hasDecorators = true;
117
+ }
118
+ if (ts2.isImportDeclaration(node)) {
119
+ const moduleSpecifier = node.moduleSpecifier.text;
120
+ const isTypeOnly = node.importClause?.isTypeOnly ?? false;
121
+ if (node.importClause) {
122
+ if (node.importClause.name) {
123
+ imports.push({
124
+ localName: node.importClause.name.text,
125
+ importedName: "default",
126
+ moduleSpecifier,
127
+ kind: "default",
128
+ isTypeOnly
129
+ });
130
+ }
131
+ if (node.importClause.namedBindings) {
132
+ if (ts2.isNamedImports(node.importClause.namedBindings)) {
133
+ node.importClause.namedBindings.elements.forEach((el) => {
134
+ imports.push({
135
+ localName: el.name.text,
136
+ importedName: el.propertyName ? el.propertyName.text : el.name.text,
137
+ moduleSpecifier,
138
+ kind: "named",
139
+ isTypeOnly: isTypeOnly || el.isTypeOnly
140
+ });
141
+ });
142
+ } else if (ts2.isNamespaceImport(node.importClause.namedBindings)) {
143
+ imports.push({
144
+ localName: node.importClause.namedBindings.name.text,
145
+ importedName: "*",
146
+ moduleSpecifier,
147
+ kind: "namespace",
148
+ isTypeOnly
149
+ });
150
+ }
151
+ }
152
+ } else {
153
+ imports.push({
154
+ localName: "",
155
+ importedName: "",
156
+ moduleSpecifier,
157
+ kind: "side_effect",
158
+ isTypeOnly: false
159
+ });
160
+ }
161
+ }
162
+ if (ts2.isCallExpression(node) && node.expression.kind === ts2.SyntaxKind.ImportKeyword) {
163
+ if (node.arguments.length > 0 && ts2.isStringLiteral(node.arguments[0])) {
164
+ const moduleSpecifier = node.arguments[0].text;
165
+ dependencyEdges.push({
166
+ fromModule: filePath,
167
+ toModule: moduleSpecifier,
168
+ // Will resolve properly later if needed
169
+ symbols: [],
170
+ isTypeOnly: false,
171
+ isDynamic: true
172
+ });
173
+ }
174
+ }
175
+ if (ts2.isExportDeclaration(node)) {
176
+ const isTypeOnly = node.isTypeOnly;
177
+ if (node.exportClause && ts2.isNamedExports(node.exportClause)) {
178
+ node.exportClause.elements.forEach((el) => {
179
+ exports.push({
180
+ localName: el.propertyName ? el.propertyName.text : el.name.text,
181
+ exportedName: el.name.text,
182
+ kind: TypeFactKind2.Variable,
183
+ // Approximate, could be refined
184
+ isTypeOnly: isTypeOnly || el.isTypeOnly,
185
+ isDefault: el.name.text === "default",
186
+ isReExport: !!node.moduleSpecifier,
187
+ sourceModule: node.moduleSpecifier ? node.moduleSpecifier.text : void 0
188
+ });
189
+ });
190
+ }
191
+ } else if (ts2.canHaveModifiers(node) && ts2.getModifiers(node)?.some((m) => m.kind === ts2.SyntaxKind.ExportKeyword)) {
192
+ const isDefault = ts2.getModifiers(node).some((m) => m.kind === ts2.SyntaxKind.DefaultKeyword);
193
+ let name = "default";
194
+ if (!isDefault && node.name && ts2.isIdentifier(node.name)) {
195
+ name = node.name.text;
196
+ } else if (!isDefault && ts2.isVariableStatement(node)) {
197
+ node.declarationList.declarations.forEach((d) => {
198
+ if (ts2.isIdentifier(d.name)) {
199
+ exports.push({
200
+ localName: d.name.text,
201
+ exportedName: d.name.text,
202
+ kind: TypeFactKind2.Variable,
203
+ isTypeOnly: false,
204
+ isDefault: false,
205
+ isReExport: false
206
+ });
207
+ }
208
+ });
209
+ return;
210
+ }
211
+ if (node.name || isDefault) {
212
+ exports.push({
213
+ localName: isDefault && node.name ? node.name.text : name,
214
+ exportedName: name,
215
+ kind: TypeFactKind2.Function,
216
+ // Approximate
217
+ isTypeOnly: ts2.isInterfaceDeclaration(node) || ts2.isTypeAliasDeclaration(node),
218
+ isDefault,
219
+ isReExport: false
220
+ });
221
+ }
222
+ }
223
+ ts2.forEachChild(node, visit);
224
+ });
225
+ const isEntryPoint = exports.length > 0;
226
+ if (isEntryPoint) entries.push(filePath);
227
+ modules.set(filePath, {
228
+ filePath,
229
+ relativePath: program.getCurrentDirectory() ? filePath.replace(program.getCurrentDirectory(), "") : filePath,
230
+ exports,
231
+ imports,
232
+ typeFacts,
233
+ isEntryPoint,
234
+ isDeclarationFile: sf.isDeclarationFile,
235
+ hasJSX,
236
+ hasDecorators,
237
+ byteSize: sf.text.length
238
+ });
239
+ }
240
+ for (const [filePath, mod] of modules.entries()) {
241
+ const targetMap = /* @__PURE__ */ new Map();
242
+ for (const imp of mod.imports) {
243
+ const target = imp.moduleSpecifier;
244
+ if (!targetMap.has(target)) {
245
+ targetMap.set(target, { symbols: [], isTypeOnly: true });
246
+ }
247
+ const entry = targetMap.get(target);
248
+ entry.symbols.push(imp.importedName);
249
+ if (!imp.isTypeOnly) entry.isTypeOnly = false;
250
+ }
251
+ for (const [target, info] of targetMap.entries()) {
252
+ dependencyEdges.push({
253
+ fromModule: filePath,
254
+ toModule: target,
255
+ symbols: info.symbols,
256
+ isTypeOnly: info.isTypeOnly,
257
+ isDynamic: false
258
+ });
259
+ }
260
+ }
261
+ return { modules, dependencyEdges, entries };
262
+ }
263
+
264
+ // src/symbol-table.ts
265
+ import { ScopeKind } from "@tsvm/shared";
266
+ function buildSymbolTable(modules) {
267
+ const symbolTable = [];
268
+ const aliases = /* @__PURE__ */ new Map();
269
+ let nextSymbolId = 1;
270
+ for (const [filePath, mod] of modules.entries()) {
271
+ for (const fact of mod.typeFacts) {
272
+ const normalizedId = nextSymbolId++;
273
+ const normalizedFact = {
274
+ ...fact,
275
+ symbolId: normalizedId
276
+ };
277
+ symbolTable.push(normalizedFact);
278
+ aliases.set(normalizedId, {
279
+ originalName: fact.symbolName,
280
+ obfuscatedName: fact.symbolName,
281
+ // Will be changed by SymbolIndirectionPass
282
+ scope: fact.isExported ? ScopeKind.Module : ScopeKind.Block,
283
+ // Simplified
284
+ symbolId: normalizedId,
285
+ isExported: fact.isExported
286
+ });
287
+ }
288
+ }
289
+ return { symbolTable, aliases };
290
+ }
291
+
292
+ // src/project.ts
293
+ import path from "path";
294
+ function analyzeProject(tsconfigPath, entryPoints) {
295
+ const diagnostics = [];
296
+ const configFile = ts3.readConfigFile(tsconfigPath, ts3.sys.readFile);
297
+ if (configFile.error) {
298
+ throw new Error(`Failed to read tsconfig: ${configFile.error.messageText}`);
299
+ }
300
+ const parsedConfig = ts3.parseJsonConfigFileContent(configFile.config, ts3.sys, path.dirname(tsconfigPath));
301
+ if (parsedConfig.errors.length > 0) {
302
+ const errorMsg = parsedConfig.errors.map((e) => e.messageText).join(", ");
303
+ throw new Error(`Invalid tsconfig: ${errorMsg}`);
304
+ }
305
+ const rootNames = entryPoints && entryPoints.length > 0 ? entryPoints : parsedConfig.fileNames;
306
+ const program = ts3.createProgram({
307
+ rootNames,
308
+ options: parsedConfig.options
309
+ });
310
+ const checker = program.getTypeChecker();
311
+ const { modules, dependencyEdges, entries } = buildModuleGraph(program, checker);
312
+ const { symbolTable, aliases } = buildSymbolTable(modules);
313
+ const tsDiagnostics = ts3.getPreEmitDiagnostics(program);
314
+ for (const diag of tsDiagnostics) {
315
+ diagnostics.push({
316
+ severity: diag.category === 1 ? DiagnosticSeverity.Error : diag.category === 0 ? DiagnosticSeverity.Warning : DiagnosticSeverity.Info,
317
+ code: `TS${diag.code}`,
318
+ message: ts3.flattenDiagnosticMessageText(diag.messageText, "\n"),
319
+ location: diag.file ? {
320
+ filePath: diag.file.fileName,
321
+ line: diag.file.getLineAndCharacterOfPosition(diag.start).line,
322
+ column: diag.file.getLineAndCharacterOfPosition(diag.start).character,
323
+ offset: diag.start,
324
+ length: diag.length
325
+ } : void 0
326
+ });
327
+ }
328
+ return {
329
+ rootDir: program.getCurrentDirectory(),
330
+ modules,
331
+ dependencyEdges,
332
+ entryPoints: entries,
333
+ symbolTable,
334
+ aliases,
335
+ compilerOptions: parsedConfig.options,
336
+ diagnostics
337
+ };
338
+ }
339
+ export {
340
+ analyzeProject,
341
+ buildModuleGraph,
342
+ buildSymbolTable,
343
+ extractTypeFacts
344
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@tsvm/ts-semantics",
3
+ "version": "0.1.0",
4
+ "files": [
5
+ "dist",
6
+ "LICENSE"
7
+ ],
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "tsup ./src/index.ts --format esm --dts --clean",
23
+ "test": "pnpm --dir ../.. exec vitest run packages/ts-semantics/tests/**/*.test.ts --config vitest.config.mjs",
24
+ "typecheck": "tsc --noEmit",
25
+ "clean": "rm -rf dist"
26
+ },
27
+ "dependencies": {
28
+ "@tsvm/shared": "workspace:*",
29
+ "typescript": "^5.8.0"
30
+ },
31
+ "devDependencies": {
32
+ "tsup": "^8.4.0"
33
+ }
34
+ }