@deepseek-ai/dsh-typert-generator 0.1.2-rc.1 → 0.1.5-alpha.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/README.i18n.yaml +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/lib/index.js +108 -33
- package/lib/types/analyzer.js +118 -54
- package/package.json +3 -3
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/typert/generator/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 48d79dfc44f6c2f658e8aa13836e5044d6cbf25d
|
|
6
|
+
README.zh.md: e546661fe297d09367185585e43d486cd417090d
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`dsh-typert-generator`
|
|
12
|
+
`dsh-typert-generator` lets maintainers turn public TypeScript types into build artifacts and compiler-independent models. Packages opt in through the `./typert` and optional `./client/typert` exports, and generation rejects declarations, publish lists, Remote exports, or Zod projections that it cannot represent correctly. Repository builds can emit executable schemas and matching declarations, while tools can call `WorkspaceAnalyzer` for inspection or catalog generation without publishing artifacts. Generation runs only at build time and never in a live agent session.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -79,7 +79,7 @@ The generator is built on one separation: extraction and emission are decoupled
|
|
|
79
79
|
|
|
80
80
|
### Analysis and faces
|
|
81
81
|
|
|
82
|
-
Host and client are independent TypeScript programs. Direct project references establish compiler-face membership, while `dsh.client` package subpaths establish runtime-face contribution; `package.json#exports` marks every cross-package public boundary, and imports or re-exports are the only cross-face edges. `check` mode fails on syntax or semantic diagnostics, missing public annotations, private cross-package references, and reachable declaration merges the model cannot retain losslessly; `write` mode inserts checker-derived annotations and returns a clean check-mode model. Types owned by NPM dependencies remain `external` references instead of being expanded.
|
|
82
|
+
Host and client are independent TypeScript programs. Direct project references establish compiler-face membership, while `dsh.client` package subpaths establish runtime-face contribution; `package.json#exports` marks every cross-package public boundary, and imports or re-exports are the only cross-face edges. A relative import that resolves inside the referencing package is followed through that module's re-exports until a package specifier appears, so package-local forwarding modules keep their original declaration references; a relative import that resolves into another package fails. `check` mode fails on syntax or semantic diagnostics, missing public annotations, private cross-package references, and reachable declaration merges the model cannot retain losslessly; `write` mode inserts checker-derived annotations and returns a clean check-mode model. Types owned by NPM dependencies remain `external` references instead of being expanded.
|
|
83
83
|
|
|
84
84
|
### Emission and publication contract
|
|
85
85
|
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-library"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`dsh-typert-generator`
|
|
12
|
+
`dsh-typert-generator` 让维护者把公开的 TypeScript 类型转换为构建产物和与编译器无关的模型。包通过 `./typert` 和可选的 `./client/typert` 导出选择加入;如果声明、发布清单、Remote 导出或 Zod 投影无法被正确表示,生成过程就会失败。仓库构建可以生成可执行 schema 与配套声明,工具也可以调用 `WorkspaceAnalyzer` 完成检查或目录生成而不发布产物。生成过程只在构建时运行,绝不会进入实时 agent 会话。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -79,7 +79,7 @@ files:
|
|
|
79
79
|
|
|
80
80
|
### 分析与 face
|
|
81
81
|
|
|
82
|
-
Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定编译器 face 的成员归属,`dsh.client` 包子路径则确定运行时 face 的贡献;`package.json#exports` 划定所有跨包公开边界,跨 face
|
|
82
|
+
Host 与 Client 是两个独立的 TypeScript 程序。直接项目引用确定编译器 face 的成员归属,`dsh.client` 包子路径则确定运行时 face 的贡献;`package.json#exports` 划定所有跨包公开边界,跨 face 的边只能来自导入或重新导出。解析到本包内部模块的相对导入会沿该模块的重新导出继续追踪,直到出现包说明符,因此包内转发模块保留原始声明引用;解析到其他包的相对导入会失败。`check` 模式遇到语法或语义诊断、缺失的公开类型标注、跨包私有引用,以及模型无法无损保留的可达声明合并时都会失败;`write` 模式插入类型检查器推导出的标注,并返回无诊断的 check 模式模型。NPM 依赖拥有的类型继续以 `external` 引用表示,不会被展开。
|
|
83
83
|
|
|
84
84
|
### 生成与发布约定
|
|
85
85
|
|
package/lib/index.js
CHANGED
|
@@ -148,15 +148,17 @@ var WorkspaceAnalyzer = class WorkspaceAnalyzer {
|
|
|
148
148
|
incremental: false,
|
|
149
149
|
noEmit: true
|
|
150
150
|
};
|
|
151
|
+
const host = this.caches.programHost(face, options);
|
|
151
152
|
const program = ts.createProgram({
|
|
152
153
|
rootNames,
|
|
153
154
|
options,
|
|
154
|
-
host
|
|
155
|
+
host
|
|
155
156
|
});
|
|
156
157
|
faces.push(new FaceAnalyzer({
|
|
157
158
|
root: this.options.root,
|
|
158
159
|
face,
|
|
159
160
|
program,
|
|
161
|
+
host,
|
|
160
162
|
registrations,
|
|
161
163
|
allRegistrations: this.registrations,
|
|
162
164
|
mode: this.options.mode,
|
|
@@ -355,6 +357,7 @@ var FaceAnalyzer = class {
|
|
|
355
357
|
root;
|
|
356
358
|
face;
|
|
357
359
|
program;
|
|
360
|
+
host;
|
|
358
361
|
checker;
|
|
359
362
|
registrations;
|
|
360
363
|
allRegistrations;
|
|
@@ -373,6 +376,7 @@ var FaceAnalyzer = class {
|
|
|
373
376
|
this.root = options.root;
|
|
374
377
|
this.face = options.face;
|
|
375
378
|
this.program = options.program;
|
|
379
|
+
this.host = options.host;
|
|
376
380
|
this.checker = options.program.getTypeChecker();
|
|
377
381
|
this.registrations = options.registrations;
|
|
378
382
|
this.allRegistrations = options.allRegistrations;
|
|
@@ -524,15 +528,78 @@ var FaceAnalyzer = class {
|
|
|
524
528
|
reachable.set(fileName, sourceFile);
|
|
525
529
|
for (const statement of sourceFile.statements) {
|
|
526
530
|
if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement) || statement.moduleSpecifier === void 0 || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
527
|
-
const
|
|
528
|
-
if (
|
|
529
|
-
const resolvedPath = realPath(resolved.resolvedFileName);
|
|
530
|
-
if (!isWithin(resolvedPath, registration.root)) continue;
|
|
531
|
+
const resolvedPath = this.resolveImport(statement.moduleSpecifier.text, sourceFile.fileName);
|
|
532
|
+
if (resolvedPath === void 0 || !isWithin(resolvedPath, registration.root)) continue;
|
|
531
533
|
queue.push(this.sourceFiles.get(resolvedPath));
|
|
532
534
|
}
|
|
533
535
|
}
|
|
534
536
|
return [...reachable.values()].sort((left, right) => left.fileName.localeCompare(right.fileName));
|
|
535
537
|
}
|
|
538
|
+
resolveImport(specifier, fromFile) {
|
|
539
|
+
const resolved = ts.resolveModuleName(specifier, fromFile, this.program.getCompilerOptions(), this.host, this.host.getModuleResolutionCache?.()).resolvedModule;
|
|
540
|
+
return resolved === void 0 ? void 0 : realPath(resolved.resolvedFileName);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Follow the import that names `symbol` at `site` through modules of the
|
|
544
|
+
* referencing package until a package specifier appears. Each forwarding
|
|
545
|
+
* module and requested export name pair is entered once; its explicit export
|
|
546
|
+
* edges are tried before its star edges. A relative specifier that resolves
|
|
547
|
+
* outside `from`, a namespace hop, or a module with no edge leading to a
|
|
548
|
+
* package specifier yields undefined.
|
|
549
|
+
*/
|
|
550
|
+
packageImportOf(site, moduleSpecifier, symbol, from) {
|
|
551
|
+
const visited = /* @__PURE__ */ new Map();
|
|
552
|
+
const walk = (sourceFile, specifier, name) => {
|
|
553
|
+
const module = moduleIdentity(specifier);
|
|
554
|
+
if (module !== void 0) return {
|
|
555
|
+
module,
|
|
556
|
+
name
|
|
557
|
+
};
|
|
558
|
+
const resolvedPath = this.resolveImport(specifier, sourceFile.fileName);
|
|
559
|
+
if (resolvedPath === void 0 || !isWithin(resolvedPath, from.root)) return void 0;
|
|
560
|
+
const names = visited.get(resolvedPath);
|
|
561
|
+
if (names?.has(name)) return void 0;
|
|
562
|
+
if (names === void 0) visited.set(resolvedPath, new Set([name]));
|
|
563
|
+
else names.add(name);
|
|
564
|
+
const forward = this.sourceFiles.get(resolvedPath);
|
|
565
|
+
for (const edge of this.forwardedExports(forward, name, symbol)) {
|
|
566
|
+
const found = walk(forward, edge.specifier, edge.name);
|
|
567
|
+
if (found !== void 0) return found;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
return walk(site.getSourceFile(), moduleSpecifier, authoredExportName(site, moduleSpecifier));
|
|
571
|
+
}
|
|
572
|
+
/** Export edges of `sourceFile` that carry `name`: explicit edges in source order, then star edges exporting `symbol`. */
|
|
573
|
+
forwardedExports(sourceFile, name, symbol) {
|
|
574
|
+
const explicit = [];
|
|
575
|
+
const stars = [];
|
|
576
|
+
for (const statement of sourceFile.statements) {
|
|
577
|
+
if (!ts.isExportDeclaration(statement) || statement.exportClause === void 0 && statement.moduleSpecifier === void 0 || statement.exportClause !== void 0 && ts.isNamespaceExport(statement.exportClause)) continue;
|
|
578
|
+
if (statement.exportClause === void 0) {
|
|
579
|
+
const specifier = statement.moduleSpecifier.text;
|
|
580
|
+
if (this.moduleExports(statement.moduleSpecifier).find((candidate) => candidate.name === name && this.resolveSymbol(candidate) === symbol) !== void 0) stars.push({
|
|
581
|
+
specifier,
|
|
582
|
+
name
|
|
583
|
+
});
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const element = statement.exportClause.elements.find((candidate) => candidate.name.text === name);
|
|
587
|
+
if (element === void 0) continue;
|
|
588
|
+
if (statement.moduleSpecifier !== void 0) {
|
|
589
|
+
explicit.push({
|
|
590
|
+
specifier: statement.moduleSpecifier.text,
|
|
591
|
+
name: element.propertyName?.text ?? name
|
|
592
|
+
});
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const binding = importBindingOf(sourceFile, element.propertyName?.text ?? name);
|
|
596
|
+
if (binding?.name !== void 0) explicit.push({
|
|
597
|
+
specifier: binding.specifier,
|
|
598
|
+
name: binding.name
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return [...explicit, ...stars];
|
|
602
|
+
}
|
|
536
603
|
collectServices(context, records) {
|
|
537
604
|
const bySymbol = /* @__PURE__ */ new Map();
|
|
538
605
|
for (const record of records) {
|
|
@@ -1760,14 +1827,13 @@ var FaceAnalyzer = class {
|
|
|
1760
1827
|
name: symbol.name
|
|
1761
1828
|
};
|
|
1762
1829
|
const moduleSpecifier = moduleSpecifierOf(site);
|
|
1763
|
-
const module = moduleSpecifier === void 0 ? void 0 : moduleIdentity(moduleSpecifier);
|
|
1764
1830
|
const from = this.registrationForFile(site.getSourceFile().fileName);
|
|
1765
1831
|
const owner = this.registrationForFile(declaration.getSourceFile().fileName);
|
|
1766
1832
|
if (owner !== void 0) {
|
|
1767
1833
|
if (owner.name !== from.name) {
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
if (this.packageExportName(module, symbol, owner.face,
|
|
1834
|
+
const imported = moduleSpecifier === void 0 ? void 0 : this.packageImportOf(site, moduleSpecifier, symbol, from);
|
|
1835
|
+
if (imported === void 0) this.fail(site, `reference to ${symbol.name} crosses a package without an explicit package import`);
|
|
1836
|
+
if (this.packageExportName(imported.module, symbol, owner.face, imported.name) === void 0) this.fail(site, `package reference ${imported.name} is not exported by ${imported.module.package} at ${imported.module.subpath}`);
|
|
1771
1837
|
}
|
|
1772
1838
|
const typeDeclaration = declaration;
|
|
1773
1839
|
if (!this.declarationStates.has(this.symbolId(symbol))) this.ensureDeclaration(symbol, typeDeclaration);
|
|
@@ -1776,11 +1842,12 @@ var FaceAnalyzer = class {
|
|
|
1776
1842
|
symbol: this.symbolId(symbol)
|
|
1777
1843
|
};
|
|
1778
1844
|
}
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1845
|
+
const imported = moduleSpecifier === void 0 ? void 0 : this.packageImportOf(site, moduleSpecifier, symbol, from);
|
|
1846
|
+
const otherFace = (imported === void 0 ? [] : [...new Set(this.allRegistrations.filter((candidate) => candidate.name === imported.module.package).map((candidate) => candidate.face))]).find((face) => face !== this.face);
|
|
1847
|
+
if (otherFace !== void 0 && imported !== void 0) {
|
|
1848
|
+
const { module } = imported;
|
|
1849
|
+
const exportName = this.packageExportName(module, symbol, otherFace, imported.name);
|
|
1850
|
+
if (exportName === void 0) this.fail(site, `cross-face reference ${imported.name} is not exported by ${module.package} at ${module.subpath}`);
|
|
1784
1851
|
this.recordCrossFaceLink(from.name, otherFace, module, exportName);
|
|
1785
1852
|
return {
|
|
1786
1853
|
kind: "cross-face",
|
|
@@ -1790,10 +1857,10 @@ var FaceAnalyzer = class {
|
|
|
1790
1857
|
name: exportName
|
|
1791
1858
|
};
|
|
1792
1859
|
}
|
|
1793
|
-
if (
|
|
1860
|
+
if (imported !== void 0) return {
|
|
1794
1861
|
kind: "external",
|
|
1795
|
-
module: module.package,
|
|
1796
|
-
subpath: module.subpath,
|
|
1862
|
+
module: imported.module.package,
|
|
1863
|
+
subpath: imported.module.subpath,
|
|
1797
1864
|
name: symbol.name
|
|
1798
1865
|
};
|
|
1799
1866
|
const external = externalModuleIdentityForFile(declaration.getSourceFile().fileName);
|
|
@@ -1826,6 +1893,7 @@ var FaceAnalyzer = class {
|
|
|
1826
1893
|
}
|
|
1827
1894
|
packageExportName(module, symbol, face, requestedName) {
|
|
1828
1895
|
const registration = this.allRegistrations.find((candidate) => candidate.face === face && candidate.name === module.package);
|
|
1896
|
+
if (registration === void 0) return void 0;
|
|
1829
1897
|
const target = packageExportTargets(registration.manifest).find(([subpath]) => subpath === module.subpath)?.[1];
|
|
1830
1898
|
if (target === void 0) return void 0;
|
|
1831
1899
|
const sourceFile = this.sourceFiles.get(realPath(sourcePathForExport(registration.root, target)));
|
|
@@ -2203,30 +2271,37 @@ function moduleSpecifierOf(node) {
|
|
|
2203
2271
|
const symbol = ts.isTypeReferenceNode(node) ? node.typeName : node.expression;
|
|
2204
2272
|
const sourceFile = node.getSourceFile();
|
|
2205
2273
|
const first = ts.isIdentifier(symbol) ? symbol.text : symbol.getFirstToken(sourceFile)?.getText(sourceFile);
|
|
2206
|
-
|
|
2207
|
-
if (!ts.isImportDeclaration(statement) || statement.importClause === void 0 || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
2208
|
-
if (statement.importClause.name?.text === first) return statement.moduleSpecifier.text;
|
|
2209
|
-
const bindings = statement.importClause.namedBindings;
|
|
2210
|
-
if (bindings !== void 0 && ts.isNamespaceImport(bindings) && bindings.name.text === first) return statement.moduleSpecifier.text;
|
|
2211
|
-
if (bindings !== void 0 && ts.isNamedImports(bindings) && bindings.elements.some((element) => element.name.text === first)) return statement.moduleSpecifier.text;
|
|
2212
|
-
}
|
|
2274
|
+
return first === void 0 ? void 0 : importBindingOf(sourceFile, first)?.specifier;
|
|
2213
2275
|
}
|
|
2214
2276
|
function authoredExportName(node, moduleSpecifier) {
|
|
2215
2277
|
if (ts.isImportTypeNode(node)) return node.qualifier.getText().split(".")[0];
|
|
2216
2278
|
const referenced = ts.isTypeReferenceNode(node) ? node.typeName.getText().split(".") : node.expression.getText().split(".");
|
|
2217
2279
|
const localName = referenced[0];
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2280
|
+
const binding = importBindingOf(node.getSourceFile(), localName);
|
|
2281
|
+
/* v8 ignore next -- moduleSpecifierOf returns only the import inspected by importBindingOf. */
|
|
2282
|
+
if (binding === void 0) throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
|
|
2283
|
+
return binding.name ?? referenced[1];
|
|
2284
|
+
}
|
|
2285
|
+
function importBindingOf(sourceFile, localName) {
|
|
2286
|
+
for (const statement of sourceFile.statements) {
|
|
2287
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause === void 0 || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
2288
|
+
const specifier = statement.moduleSpecifier.text;
|
|
2289
|
+
if (statement.importClause.name?.text === localName) return {
|
|
2290
|
+
specifier,
|
|
2291
|
+
name: "default"
|
|
2292
|
+
};
|
|
2221
2293
|
const bindings = statement.importClause.namedBindings;
|
|
2222
|
-
if (bindings
|
|
2223
|
-
|
|
2224
|
-
if (
|
|
2294
|
+
if (bindings === void 0) continue;
|
|
2295
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
2296
|
+
if (bindings.name.text === localName) return { specifier };
|
|
2297
|
+
continue;
|
|
2225
2298
|
}
|
|
2226
|
-
|
|
2299
|
+
const element = bindings.elements.find((candidate) => candidate.name.text === localName);
|
|
2300
|
+
if (element !== void 0) return {
|
|
2301
|
+
specifier,
|
|
2302
|
+
name: element.propertyName?.text ?? element.name.text
|
|
2303
|
+
};
|
|
2227
2304
|
}
|
|
2228
|
-
/* v8 ignore next -- moduleSpecifierOf returns only the matching import inspected by this loop. */
|
|
2229
|
-
throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
|
|
2230
2305
|
}
|
|
2231
2306
|
function importTypeAttributesText(node) {
|
|
2232
2307
|
const sourceFile = node.getSourceFile();
|
package/lib/types/analyzer.js
CHANGED
|
@@ -164,15 +164,13 @@ export class WorkspaceAnalyzer {
|
|
|
164
164
|
incremental: false,
|
|
165
165
|
noEmit: true,
|
|
166
166
|
};
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
options,
|
|
170
|
-
host: this.caches.programHost(face, options),
|
|
171
|
-
});
|
|
167
|
+
const host = this.caches.programHost(face, options);
|
|
168
|
+
const program = ts.createProgram({ rootNames, options, host });
|
|
172
169
|
faces.push(new FaceAnalyzer({
|
|
173
170
|
root: this.options.root,
|
|
174
171
|
face,
|
|
175
172
|
program,
|
|
173
|
+
host,
|
|
176
174
|
registrations,
|
|
177
175
|
allRegistrations: this.registrations,
|
|
178
176
|
mode: this.options.mode,
|
|
@@ -425,6 +423,7 @@ class FaceAnalyzer {
|
|
|
425
423
|
root;
|
|
426
424
|
face;
|
|
427
425
|
program;
|
|
426
|
+
host;
|
|
428
427
|
checker;
|
|
429
428
|
registrations;
|
|
430
429
|
allRegistrations;
|
|
@@ -443,6 +442,7 @@ class FaceAnalyzer {
|
|
|
443
442
|
this.root = options.root;
|
|
444
443
|
this.face = options.face;
|
|
445
444
|
this.program = options.program;
|
|
445
|
+
this.host = options.host;
|
|
446
446
|
this.checker = options.program.getTypeChecker();
|
|
447
447
|
this.registrations = options.registrations;
|
|
448
448
|
this.allRegistrations = options.allRegistrations;
|
|
@@ -651,17 +651,82 @@ class FaceAnalyzer {
|
|
|
651
651
|
|| statement.moduleSpecifier === undefined
|
|
652
652
|
|| !ts.isStringLiteral(statement.moduleSpecifier))
|
|
653
653
|
continue;
|
|
654
|
-
const
|
|
655
|
-
if (
|
|
656
|
-
continue;
|
|
657
|
-
const resolvedPath = realPath(resolved.resolvedFileName);
|
|
658
|
-
if (!isWithin(resolvedPath, registration.root))
|
|
654
|
+
const resolvedPath = this.resolveImport(statement.moduleSpecifier.text, sourceFile.fileName);
|
|
655
|
+
if (resolvedPath === undefined || !isWithin(resolvedPath, registration.root))
|
|
659
656
|
continue;
|
|
660
657
|
queue.push(this.sourceFiles.get(resolvedPath));
|
|
661
658
|
}
|
|
662
659
|
}
|
|
663
660
|
return [...reachable.values()].sort((left, right) => left.fileName.localeCompare(right.fileName));
|
|
664
661
|
}
|
|
662
|
+
resolveImport(specifier, fromFile) {
|
|
663
|
+
const resolved = ts.resolveModuleName(specifier, fromFile, this.program.getCompilerOptions(), this.host, this.host.getModuleResolutionCache?.()).resolvedModule;
|
|
664
|
+
return resolved === undefined ? undefined : realPath(resolved.resolvedFileName);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Follow the import that names `symbol` at `site` through modules of the
|
|
668
|
+
* referencing package until a package specifier appears. Each forwarding
|
|
669
|
+
* module and requested export name pair is entered once; its explicit export
|
|
670
|
+
* edges are tried before its star edges. A relative specifier that resolves
|
|
671
|
+
* outside `from`, a namespace hop, or a module with no edge leading to a
|
|
672
|
+
* package specifier yields undefined.
|
|
673
|
+
*/
|
|
674
|
+
packageImportOf(site, moduleSpecifier, symbol, from) {
|
|
675
|
+
const visited = new Map();
|
|
676
|
+
const walk = (sourceFile, specifier, name) => {
|
|
677
|
+
const module = moduleIdentity(specifier);
|
|
678
|
+
if (module !== undefined)
|
|
679
|
+
return { module, name };
|
|
680
|
+
const resolvedPath = this.resolveImport(specifier, sourceFile.fileName);
|
|
681
|
+
if (resolvedPath === undefined || !isWithin(resolvedPath, from.root))
|
|
682
|
+
return undefined;
|
|
683
|
+
const names = visited.get(resolvedPath);
|
|
684
|
+
if (names?.has(name))
|
|
685
|
+
return undefined;
|
|
686
|
+
if (names === undefined)
|
|
687
|
+
visited.set(resolvedPath, new Set([name]));
|
|
688
|
+
else
|
|
689
|
+
names.add(name);
|
|
690
|
+
const forward = this.sourceFiles.get(resolvedPath);
|
|
691
|
+
for (const edge of this.forwardedExports(forward, name, symbol)) {
|
|
692
|
+
const found = walk(forward, edge.specifier, edge.name);
|
|
693
|
+
if (found !== undefined)
|
|
694
|
+
return found;
|
|
695
|
+
}
|
|
696
|
+
return undefined;
|
|
697
|
+
};
|
|
698
|
+
return walk(site.getSourceFile(), moduleSpecifier, authoredExportName(site, moduleSpecifier));
|
|
699
|
+
}
|
|
700
|
+
/** Export edges of `sourceFile` that carry `name`: explicit edges in source order, then star edges exporting `symbol`. */
|
|
701
|
+
forwardedExports(sourceFile, name, symbol) {
|
|
702
|
+
const explicit = [];
|
|
703
|
+
const stars = [];
|
|
704
|
+
for (const statement of sourceFile.statements) {
|
|
705
|
+
if (!ts.isExportDeclaration(statement)
|
|
706
|
+
|| (statement.exportClause === undefined && statement.moduleSpecifier === undefined)
|
|
707
|
+
|| (statement.exportClause !== undefined && ts.isNamespaceExport(statement.exportClause)))
|
|
708
|
+
continue;
|
|
709
|
+
if (statement.exportClause === undefined) {
|
|
710
|
+
const specifier = statement.moduleSpecifier.text;
|
|
711
|
+
const exported = this.moduleExports(statement.moduleSpecifier)
|
|
712
|
+
.find(candidate => candidate.name === name && this.resolveSymbol(candidate) === symbol);
|
|
713
|
+
if (exported !== undefined)
|
|
714
|
+
stars.push({ specifier, name });
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const element = statement.exportClause.elements.find(candidate => candidate.name.text === name);
|
|
718
|
+
if (element === undefined)
|
|
719
|
+
continue;
|
|
720
|
+
if (statement.moduleSpecifier !== undefined) {
|
|
721
|
+
explicit.push({ specifier: statement.moduleSpecifier.text, name: element.propertyName?.text ?? name });
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const binding = importBindingOf(sourceFile, element.propertyName?.text ?? name);
|
|
725
|
+
if (binding?.name !== undefined)
|
|
726
|
+
explicit.push({ specifier: binding.specifier, name: binding.name });
|
|
727
|
+
}
|
|
728
|
+
return [...explicit, ...stars];
|
|
729
|
+
}
|
|
665
730
|
collectServices(context, records) {
|
|
666
731
|
const bySymbol = new Map();
|
|
667
732
|
for (const record of records) {
|
|
@@ -2125,17 +2190,18 @@ class FaceAnalyzer {
|
|
|
2125
2190
|
return { kind: 'standard', name: symbol.name };
|
|
2126
2191
|
}
|
|
2127
2192
|
const moduleSpecifier = moduleSpecifierOf(site);
|
|
2128
|
-
const module = moduleSpecifier === undefined ? undefined : moduleIdentity(moduleSpecifier);
|
|
2129
2193
|
const from = this.registrationForFile(site.getSourceFile().fileName);
|
|
2130
2194
|
const owner = this.registrationForFile(declaration.getSourceFile().fileName);
|
|
2131
2195
|
if (owner !== undefined) {
|
|
2132
2196
|
if (owner.name !== from.name) {
|
|
2133
|
-
|
|
2197
|
+
const imported = moduleSpecifier === undefined
|
|
2198
|
+
? undefined
|
|
2199
|
+
: this.packageImportOf(site, moduleSpecifier, symbol, from);
|
|
2200
|
+
if (imported === undefined) {
|
|
2134
2201
|
this.fail(site, `reference to ${symbol.name} crosses a package without an explicit package import`);
|
|
2135
2202
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
this.fail(site, `package reference ${exportName} is not exported by ${module.package} at ${module.subpath}`);
|
|
2203
|
+
if (this.packageExportName(imported.module, symbol, owner.face, imported.name) === undefined) {
|
|
2204
|
+
this.fail(site, `package reference ${imported.name} is not exported by ${imported.module.package} at ${imported.module.subpath}`);
|
|
2139
2205
|
}
|
|
2140
2206
|
}
|
|
2141
2207
|
const typeDeclaration = declaration;
|
|
@@ -2143,15 +2209,18 @@ class FaceAnalyzer {
|
|
|
2143
2209
|
this.ensureDeclaration(symbol, typeDeclaration);
|
|
2144
2210
|
return { kind: 'declaration', symbol: this.symbolId(symbol) };
|
|
2145
2211
|
}
|
|
2146
|
-
const
|
|
2212
|
+
const imported = moduleSpecifier === undefined
|
|
2213
|
+
? undefined
|
|
2214
|
+
: this.packageImportOf(site, moduleSpecifier, symbol, from);
|
|
2215
|
+
const packageFaces = imported === undefined
|
|
2147
2216
|
? []
|
|
2148
|
-
: [...new Set(this.allRegistrations.filter(candidate => candidate.name === module.package).map(candidate => candidate.face))];
|
|
2217
|
+
: [...new Set(this.allRegistrations.filter(candidate => candidate.name === imported.module.package).map(candidate => candidate.face))];
|
|
2149
2218
|
const otherFace = packageFaces.find(face => face !== this.face);
|
|
2150
|
-
if (otherFace !== undefined &&
|
|
2151
|
-
const
|
|
2152
|
-
const exportName = this.packageExportName(module, symbol, otherFace,
|
|
2219
|
+
if (otherFace !== undefined && imported !== undefined) {
|
|
2220
|
+
const { module } = imported;
|
|
2221
|
+
const exportName = this.packageExportName(module, symbol, otherFace, imported.name);
|
|
2153
2222
|
if (exportName === undefined) {
|
|
2154
|
-
this.fail(site, `cross-face reference ${
|
|
2223
|
+
this.fail(site, `cross-face reference ${imported.name} is not exported by ${module.package} at ${module.subpath}`);
|
|
2155
2224
|
}
|
|
2156
2225
|
this.recordCrossFaceLink(from.name, otherFace, module, exportName);
|
|
2157
2226
|
return {
|
|
@@ -2162,11 +2231,11 @@ class FaceAnalyzer {
|
|
|
2162
2231
|
name: exportName,
|
|
2163
2232
|
};
|
|
2164
2233
|
}
|
|
2165
|
-
if (
|
|
2234
|
+
if (imported !== undefined) {
|
|
2166
2235
|
return {
|
|
2167
2236
|
kind: 'external',
|
|
2168
|
-
module: module.package,
|
|
2169
|
-
subpath: module.subpath,
|
|
2237
|
+
module: imported.module.package,
|
|
2238
|
+
subpath: imported.module.subpath,
|
|
2170
2239
|
name: symbol.name,
|
|
2171
2240
|
};
|
|
2172
2241
|
}
|
|
@@ -2202,6 +2271,8 @@ class FaceAnalyzer {
|
|
|
2202
2271
|
}
|
|
2203
2272
|
packageExportName(module, symbol, face, requestedName) {
|
|
2204
2273
|
const registration = this.allRegistrations.find(candidate => candidate.face === face && candidate.name === module.package);
|
|
2274
|
+
if (registration === undefined)
|
|
2275
|
+
return undefined;
|
|
2205
2276
|
const target = packageExportTargets(registration.manifest)
|
|
2206
2277
|
.find(([subpath]) => subpath === module.subpath)?.[1];
|
|
2207
2278
|
if (target === undefined)
|
|
@@ -2678,21 +2749,7 @@ function moduleSpecifierOf(node) {
|
|
|
2678
2749
|
: node.expression;
|
|
2679
2750
|
const sourceFile = node.getSourceFile();
|
|
2680
2751
|
const first = ts.isIdentifier(symbol) ? symbol.text : symbol.getFirstToken(sourceFile)?.getText(sourceFile);
|
|
2681
|
-
|
|
2682
|
-
if (!ts.isImportDeclaration(statement) || statement.importClause === undefined
|
|
2683
|
-
|| !ts.isStringLiteral(statement.moduleSpecifier))
|
|
2684
|
-
continue;
|
|
2685
|
-
if (statement.importClause.name?.text === first)
|
|
2686
|
-
return statement.moduleSpecifier.text;
|
|
2687
|
-
const bindings = statement.importClause.namedBindings;
|
|
2688
|
-
if (bindings !== undefined && ts.isNamespaceImport(bindings) && bindings.name.text === first) {
|
|
2689
|
-
return statement.moduleSpecifier.text;
|
|
2690
|
-
}
|
|
2691
|
-
if (bindings !== undefined && ts.isNamedImports(bindings)
|
|
2692
|
-
&& bindings.elements.some(element => element.name.text === first))
|
|
2693
|
-
return statement.moduleSpecifier.text;
|
|
2694
|
-
}
|
|
2695
|
-
return undefined;
|
|
2752
|
+
return first === undefined ? undefined : importBindingOf(sourceFile, first)?.specifier;
|
|
2696
2753
|
}
|
|
2697
2754
|
function authoredExportName(node, moduleSpecifier) {
|
|
2698
2755
|
if (ts.isImportTypeNode(node))
|
|
@@ -2701,26 +2758,33 @@ function authoredExportName(node, moduleSpecifier) {
|
|
|
2701
2758
|
? node.typeName.getText().split('.')
|
|
2702
2759
|
: node.expression.getText().split('.');
|
|
2703
2760
|
const localName = referenced[0];
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2761
|
+
const binding = importBindingOf(node.getSourceFile(), localName);
|
|
2762
|
+
/* v8 ignore next -- moduleSpecifierOf returns only the import inspected by importBindingOf. */
|
|
2763
|
+
if (binding === undefined)
|
|
2764
|
+
throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
|
|
2765
|
+
return binding.name ?? referenced[1];
|
|
2766
|
+
}
|
|
2767
|
+
function importBindingOf(sourceFile, localName) {
|
|
2768
|
+
for (const statement of sourceFile.statements) {
|
|
2769
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause === undefined
|
|
2770
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier))
|
|
2709
2771
|
continue;
|
|
2772
|
+
const specifier = statement.moduleSpecifier.text;
|
|
2710
2773
|
if (statement.importClause.name?.text === localName)
|
|
2711
|
-
return 'default';
|
|
2774
|
+
return { specifier, name: 'default' };
|
|
2712
2775
|
const bindings = statement.importClause.namedBindings;
|
|
2713
|
-
if (bindings
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
return referenced[1];
|
|
2776
|
+
if (bindings === undefined)
|
|
2777
|
+
continue;
|
|
2778
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
2779
|
+
if (bindings.name.text === localName)
|
|
2780
|
+
return { specifier };
|
|
2781
|
+
continue;
|
|
2720
2782
|
}
|
|
2783
|
+
const element = bindings.elements.find(candidate => candidate.name.text === localName);
|
|
2784
|
+
if (element !== undefined)
|
|
2785
|
+
return { specifier, name: element.propertyName?.text ?? element.name.text };
|
|
2721
2786
|
}
|
|
2722
|
-
|
|
2723
|
-
throw new TypertAnalysisError(`typert: cannot recover export name for ${localName} from ${moduleSpecifier}`);
|
|
2787
|
+
return undefined;
|
|
2724
2788
|
}
|
|
2725
2789
|
function importTypeAttributesText(node) {
|
|
2726
2790
|
const sourceFile = node.getSourceFile();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-typert-generator",
|
|
3
3
|
"description": "TypeScript project analyzer and model-driven Typert artifact generator",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5-alpha.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"zod": "^4.4.3",
|
|
43
|
-
"@deepseek-ai/dsh-tool-cordis": "^0.1.
|
|
44
|
-
"@deepseek-ai/dsh-typert-registry": "^0.1.
|
|
43
|
+
"@deepseek-ai/dsh-tool-cordis": "^0.1.5-alpha.1",
|
|
44
|
+
"@deepseek-ai/dsh-typert-registry": "^0.1.5-alpha.1",
|
|
45
45
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
46
46
|
}
|
|
47
47
|
}
|