@contractkit/plugin-typescript 0.20.0 → 0.21.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.
- package/.turbo/turbo-build$colon$ci.log +6 -6
- package/.turbo/turbo-test$colon$ci.log +15 -15
- package/CHANGELOG.md +12 -0
- package/coverage/clover.xml +418 -384
- package/coverage/coverage-final.json +3 -3
- package/coverage/index.html +19 -19
- package/coverage/src/codegen-contract.ts.html +1 -1
- package/coverage/src/codegen-operation.ts.html +1 -1
- package/coverage/src/codegen-plain-types.ts.html +1 -1
- package/coverage/src/codegen-sdk.ts.html +291 -180
- package/coverage/src/index.html +38 -38
- package/coverage/src/index.ts.html +249 -78
- package/coverage/src/path-utils.ts.html +109 -13
- package/coverage/src/ts-render.ts.html +1 -1
- package/coverage/tests/helpers.ts.html +1 -1
- package/coverage/tests/index.html +1 -1
- package/dist/codegen-sdk.d.ts +40 -15
- package/dist/codegen-sdk.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +175 -94
- package/dist/index.js.map +1 -1
- package/dist/path-utils.d.ts +11 -0
- package/dist/path-utils.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/codegen-sdk.ts +133 -96
- package/src/index.ts +97 -40
- package/src/path-utils.ts +32 -0
- package/tests/codegen-sdk.test.ts +73 -78
- package/tests/codegen-server.test.ts +28 -16
package/dist/path-utils.d.ts
CHANGED
|
@@ -6,6 +6,17 @@ export declare function commonDir(files: string[], rootDir: string): string;
|
|
|
6
6
|
export declare function computeOpOutPath(filePath: string, baseDir: string, output: string | undefined, defaultSuffix: string, commonRoot: string, meta?: Record<string, string>): string;
|
|
7
7
|
export declare function computeContractOutPath(filePath: string, baseDir: string, output: string | undefined, defaultSuffix: string, commonRoot: string, meta?: Record<string, string>): string;
|
|
8
8
|
export declare function computeSdkOutPath(filePath: string, rootDir: string, clientOutput: string | undefined, commonRoot: string, meta?: Record<string, string>): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the output path for a synthesized `<area>.client.ts` — the file holding the
|
|
11
|
+
* `<Area>Client` class that aggregates an area's inlined methods and subarea wiring.
|
|
12
|
+
*
|
|
13
|
+
* Uses the same `output.clients` template as leaf clients, with `{filename}` and `{area}`
|
|
14
|
+
* substituted to the area name and `{subarea}` substituted to the empty string. Resulting
|
|
15
|
+
* double-slashes from the empty substitution are collapsed, and a final segment that
|
|
16
|
+
* would otherwise begin with a dot (e.g. `.client.ts` from `{subarea}.client.ts`) is
|
|
17
|
+
* prefixed with the area so the file isn't hidden.
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeSdkAreaClientOutPath(area: string, rootDir: string, clientOutput: string | undefined): string;
|
|
9
20
|
export declare function computeSdkTypeOutPath(filePath: string, rootDir: string, typeOutput: string, commonRoot: string, meta?: Record<string, string>): string | null;
|
|
10
21
|
export declare function generateBarrelFiles(contractPaths: string[]): {
|
|
11
22
|
outPath: string;
|
package/dist/path-utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGtE,eAAO,MAAM,eAAe,QAAY,CAAC;AAEzC,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAEtF;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAclE;AAID,wBAAgB,gBAAgB,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,CAiBR;AAED,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,CAER;AAID,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,GAAG,IAAI,CAkBf;AAED,wBAAgB,qBAAqB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,GAAG,IAAI,CAef;AAED,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,CAiBnG;AAED,wBAAgB,6BAA6B,CACzC,MAAM,EAAE,UAAU,EAAE,EACpB,YAAY,EAAE,gBAAgB,EAAE,EAChC,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,EAC5B,gBAAgB,GAAE,GAAG,CAAC,MAAM,CAAa,GAC1C,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CA0CpB"}
|
|
1
|
+
{"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGtE,eAAO,MAAM,eAAe,QAAY,CAAC;AAEzC,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAEtF;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAclE;AAID,wBAAgB,gBAAgB,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,CAiBR;AAED,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,CAER;AAID,wBAAgB,iBAAiB,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,GAAG,IAAI,CAkBf;AAED;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAoBnH;AAED,wBAAgB,qBAAqB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,GAAG,IAAI,CAef;AAED,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,CAiBnG;AAED,wBAAgB,6BAA6B,CACzC,MAAM,EAAE,UAAU,EAAE,EACpB,YAAY,EAAE,gBAAgB,EAAE,EAChC,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,EAC5B,gBAAgB,GAAE,GAAG,CAAC,MAAM,CAAa,GAC1C,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CA0CpB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractkit/plugin-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "ContractKit built-in plugin: TypeScript codegen (SDK clients, Koa routers, Zod schemas, plain types)",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Marooned Software",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"@contractkit/core": "0.16.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@repo/config-
|
|
33
|
-
"@repo/config-
|
|
32
|
+
"@repo/config-eslint": "0.3.1",
|
|
33
|
+
"@repo/config-typescript": "0.1.0"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly --declaration",
|
package/src/codegen-sdk.ts
CHANGED
|
@@ -972,23 +972,25 @@ export interface SdkClientInfo {
|
|
|
972
972
|
}
|
|
973
973
|
|
|
974
974
|
/**
|
|
975
|
-
* One area-level (no-subarea) `.ck` file whose methods are
|
|
976
|
-
*
|
|
975
|
+
* One area-level (no-subarea) `.ck` file whose methods are merged into the area's
|
|
976
|
+
* `<Area>Client` (emitted to its own `<area>.client.ts`).
|
|
977
977
|
*/
|
|
978
978
|
export interface SdkAreaInlineFile {
|
|
979
979
|
/** Parsed AST. */
|
|
980
980
|
root: OpRootNode;
|
|
981
|
-
/** Codegen options for this file (must have `outPath` pointing at the
|
|
981
|
+
/** Codegen options for this file (must have `outPath` pointing at the area client file so type-import paths resolve correctly). */
|
|
982
982
|
codegenOptions: SdkCodegenOptions;
|
|
983
983
|
}
|
|
984
984
|
|
|
985
985
|
/** A grouping of files that share the same `keys.area`. */
|
|
986
986
|
export interface SdkAreaInfo {
|
|
987
987
|
area: string;
|
|
988
|
-
/**
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
988
|
+
/**
|
|
989
|
+
* Reference to the `<Area>Client` class — the file that holds it lives at
|
|
990
|
+
* `client.importPath` (relative to `sdk.ts`) and is generated separately by
|
|
991
|
+
* {@link generateAreaClient}. The aggregator just imports it.
|
|
992
|
+
*/
|
|
993
|
+
client: SdkClientInfo;
|
|
992
994
|
}
|
|
993
995
|
|
|
994
996
|
export interface SdkAggregatorInput {
|
|
@@ -1002,88 +1004,105 @@ export interface SdkAggregatorInput {
|
|
|
1002
1004
|
sdkClassName?: string;
|
|
1003
1005
|
}
|
|
1004
1006
|
|
|
1007
|
+
/** Inputs to {@link generateAreaClient}. */
|
|
1008
|
+
export interface AreaClientInput {
|
|
1009
|
+
/** Area name (e.g. `payments`). Drives the generated class name (`PaymentsClient`). */
|
|
1010
|
+
area: string;
|
|
1011
|
+
/** Output path of the generated `<area>.client.ts` file. Used to resolve relative type / leaf-client / sdk-options imports. */
|
|
1012
|
+
outPath: string;
|
|
1013
|
+
/** Files contributing inlined methods to the area client (typically area-level files with no subarea). */
|
|
1014
|
+
inlineFiles: SdkAreaInlineFile[];
|
|
1015
|
+
/** Subarea leaf clients exposed as named properties on the area client. */
|
|
1016
|
+
subareaClients: { propertyName: string; client: SdkClientInfo }[];
|
|
1017
|
+
/** Path to `sdk-options.ts`, used for `SdkFetch` and runtime helpers. */
|
|
1018
|
+
sdkOptionsPath: string;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1005
1021
|
/**
|
|
1006
|
-
* Generate
|
|
1022
|
+
* Generate a complete `<area>.client.ts` file: the `<Area>Client` class with
|
|
1023
|
+
* subarea property fields, a constructor that wires them, and inlined methods
|
|
1024
|
+
* merged from every area-level file in `inlineFiles`.
|
|
1007
1025
|
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1010
|
-
* properties, plus all imports needed by the inlined methods.
|
|
1026
|
+
* Emitted by the plugin alongside the per-leaf `*.client.ts` files. The SDK
|
|
1027
|
+
* aggregator just imports the resulting class — see {@link generateSdkAggregator}.
|
|
1011
1028
|
*
|
|
1012
|
-
* @throws if two area-level files
|
|
1029
|
+
* @throws if two area-level files contribute the same method name to the area —
|
|
1030
|
+
* disambiguate via `sdk:` on the operation, or move one into a subarea.
|
|
1013
1031
|
*/
|
|
1014
|
-
export function
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
// ──
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1032
|
+
export function generateAreaClient(input: AreaClientInput): string {
|
|
1033
|
+
const { area, outPath, inlineFiles, subareaClients, sdkOptionsPath } = input;
|
|
1034
|
+
const className = deriveAreaClientClassName(area);
|
|
1035
|
+
|
|
1036
|
+
// ── Merge inputs across all inline files ────────────────────────────────
|
|
1037
|
+
const collectedMethodLines: string[] = [];
|
|
1038
|
+
const seenMethods = new Set<string>();
|
|
1039
|
+
const typesByImportPath = new Map<string, Set<string>>();
|
|
1021
1040
|
const unresolvedTypes = new Set<string>();
|
|
1022
1041
|
let needsJson = false;
|
|
1023
1042
|
let needsBigIntReplacer = false;
|
|
1024
1043
|
let needsBigIntReviver = false;
|
|
1025
1044
|
let needsQueryString = false;
|
|
1026
1045
|
|
|
1027
|
-
for (const
|
|
1028
|
-
const
|
|
1029
|
-
const
|
|
1030
|
-
for (const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
if (seenMethods.has(name)) {
|
|
1036
|
-
throw new Error(
|
|
1037
|
-
`[sdk] duplicate method '${name}' in area '${area.area}': two area-level files contribute the same method. Disambiguate via 'sdk:' or move one into a subarea.`,
|
|
1038
|
-
);
|
|
1039
|
-
}
|
|
1040
|
-
seenMethods.add(name);
|
|
1046
|
+
for (const inline of inlineFiles) {
|
|
1047
|
+
const includeInternal = inline.codegenOptions.includeInternal ?? false;
|
|
1048
|
+
const { lines: methodLines, methodNames } = generateClientMethods(inline.root, inline.codegenOptions);
|
|
1049
|
+
for (const name of methodNames) {
|
|
1050
|
+
if (seenMethods.has(name)) {
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
`[sdk] duplicate method '${name}' in area '${area}': two area-level files contribute the same method. Disambiguate via 'sdk:' or move one into a subarea.`,
|
|
1053
|
+
);
|
|
1041
1054
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1055
|
+
seenMethods.add(name);
|
|
1056
|
+
}
|
|
1057
|
+
collectedMethodLines.push(...methodLines);
|
|
1058
|
+
if (sdkNeedsJson(inline.root, includeInternal)) needsJson = true;
|
|
1059
|
+
if (sdkNeedsBigIntReplacer(inline.root, includeInternal)) needsBigIntReplacer = true;
|
|
1060
|
+
if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
|
|
1061
|
+
if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
|
|
1062
|
+
|
|
1063
|
+
// Resolve each file's type refs against THIS file's modelOutPaths, but
|
|
1064
|
+
// produce import paths relative to the area client's outPath (not the
|
|
1065
|
+
// contributing file's outPath, which pointed at the now-defunct sdk.ts).
|
|
1066
|
+
const typesForFile = collectTypes(
|
|
1067
|
+
inline.root,
|
|
1068
|
+
inline.codegenOptions.modelsWithInput,
|
|
1069
|
+
inline.codegenOptions.modelsWithOutput,
|
|
1070
|
+
includeInternal,
|
|
1071
|
+
);
|
|
1072
|
+
const { modelOutPaths } = inline.codegenOptions;
|
|
1073
|
+
if (modelOutPaths) {
|
|
1074
|
+
const fromDir = dirname(outPath);
|
|
1075
|
+
for (const t of typesForFile) {
|
|
1076
|
+
const typeOutPath = modelOutPaths.get(t);
|
|
1077
|
+
if (typeOutPath) {
|
|
1078
|
+
let rel = relative(fromDir, typeOutPath).replace(/\.ts$/, '.js');
|
|
1079
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
1080
|
+
const set = typesByImportPath.get(rel) ?? new Set();
|
|
1081
|
+
set.add(t);
|
|
1082
|
+
typesByImportPath.set(rel, set);
|
|
1083
|
+
} else {
|
|
1084
|
+
unresolvedTypes.add(t);
|
|
1064
1085
|
}
|
|
1065
1086
|
}
|
|
1066
1087
|
}
|
|
1067
|
-
inlinedByArea.set(area.area, { lines: collected, methodNames: seenMethods });
|
|
1068
1088
|
}
|
|
1069
1089
|
|
|
1070
|
-
// ── Imports
|
|
1090
|
+
// ── Imports ─────────────────────────────────────────────────────────────
|
|
1091
|
+
let sdkOptionsRel = relative(dirname(outPath), sdkOptionsPath).replace(/\.ts$/, '.js');
|
|
1092
|
+
if (!sdkOptionsRel.startsWith('.')) sdkOptionsRel = './' + sdkOptionsRel;
|
|
1093
|
+
|
|
1071
1094
|
const lines: string[] = [];
|
|
1072
1095
|
const jsonImport = needsJson ? ', JsonValue' : '';
|
|
1073
|
-
lines.push(`import type { SdkFetch${jsonImport} } from '${
|
|
1096
|
+
lines.push(`import type { SdkFetch${jsonImport} } from '${sdkOptionsRel}';`);
|
|
1074
1097
|
const valueImports: string[] = [];
|
|
1075
1098
|
if (needsBigIntReplacer) valueImports.push('bigIntReplacer');
|
|
1076
1099
|
if (needsBigIntReviver) valueImports.push('parseJson');
|
|
1077
1100
|
if (needsQueryString) valueImports.push('buildQueryString');
|
|
1078
1101
|
if (valueImports.length > 0) {
|
|
1079
|
-
lines.push(`import { ${valueImports.join(', ')} } from '${
|
|
1102
|
+
lines.push(`import { ${valueImports.join(', ')} } from '${sdkOptionsRel}';`);
|
|
1080
1103
|
}
|
|
1081
|
-
lines.push(`import type { SdkOptions } from '${sdkOptionsImportPath}';`);
|
|
1082
|
-
lines.push(`import { createSdkFetch } from '${sdkOptionsImportPath}';`);
|
|
1083
1104
|
|
|
1084
|
-
|
|
1085
|
-
const typeImportPaths = [...typesByImportPath.keys()].sort();
|
|
1086
|
-
for (const path of typeImportPaths) {
|
|
1105
|
+
for (const path of [...typesByImportPath.keys()].sort()) {
|
|
1087
1106
|
const names = [...typesByImportPath.get(path)!].sort();
|
|
1088
1107
|
lines.push(`import type { ${names.join(', ')} } from '${path}';`);
|
|
1089
1108
|
}
|
|
@@ -1091,7 +1110,52 @@ export function generateSdkAggregator(input: SdkAggregatorInput): string {
|
|
|
1091
1110
|
lines.push(`import type { ${t} } from './${pascalToDotCase(t)}.js';`);
|
|
1092
1111
|
}
|
|
1093
1112
|
|
|
1094
|
-
// Leaf client imports (top-level
|
|
1113
|
+
// Leaf client imports (subareas only — top-level clients live next to sdk.ts).
|
|
1114
|
+
const importedClients = new Set<string>();
|
|
1115
|
+
for (const sc of subareaClients) {
|
|
1116
|
+
const key = `${sc.client.className}|${sc.client.importPath}`;
|
|
1117
|
+
if (importedClients.has(key)) continue;
|
|
1118
|
+
importedClients.add(key);
|
|
1119
|
+
lines.push(`import { ${sc.client.className} } from '${sc.client.importPath}';`);
|
|
1120
|
+
}
|
|
1121
|
+
lines.push('');
|
|
1122
|
+
|
|
1123
|
+
// ── <Area>Client class ──────────────────────────────────────────────────
|
|
1124
|
+
lines.push(`export class ${className} {`);
|
|
1125
|
+
for (const sc of subareaClients) {
|
|
1126
|
+
lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
|
|
1127
|
+
}
|
|
1128
|
+
if (subareaClients.length > 0) lines.push('');
|
|
1129
|
+
if (collectedMethodLines.length > 0 || subareaClients.length > 0) {
|
|
1130
|
+
const fetchModifier = collectedMethodLines.length > 0 ? 'private ' : '';
|
|
1131
|
+
lines.push(` constructor(${fetchModifier}fetch: SdkFetch) {`);
|
|
1132
|
+
for (const sc of subareaClients) {
|
|
1133
|
+
lines.push(` this.${sc.propertyName} = new ${sc.client.className}(fetch);`);
|
|
1134
|
+
}
|
|
1135
|
+
lines.push(' }');
|
|
1136
|
+
}
|
|
1137
|
+
for (const ln of collectedMethodLines) lines.push(ln);
|
|
1138
|
+
lines.push('}');
|
|
1139
|
+
lines.push('');
|
|
1140
|
+
|
|
1141
|
+
return lines.join('\n');
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Generate the SDK aggregator (`sdk.ts`) — the entry-point file consumers import.
|
|
1146
|
+
*
|
|
1147
|
+
* Imports every `<Area>Client` (one per area, generated by {@link generateAreaClient}
|
|
1148
|
+
* to its own `<area>.client.ts`) and every leaf top-level client, then emits a
|
|
1149
|
+
* `class Sdk` that exposes them as properties.
|
|
1150
|
+
*/
|
|
1151
|
+
export function generateSdkAggregator(input: SdkAggregatorInput): string {
|
|
1152
|
+
const sdkOptionsImportPath = input.sdkOptionsImportPath ?? './sdk-options.js';
|
|
1153
|
+
const sdkClassName = input.sdkClassName ?? 'Sdk';
|
|
1154
|
+
|
|
1155
|
+
const lines: string[] = [];
|
|
1156
|
+
lines.push(`import type { SdkOptions } from '${sdkOptionsImportPath}';`);
|
|
1157
|
+
lines.push(`import { createSdkFetch } from '${sdkOptionsImportPath}';`);
|
|
1158
|
+
|
|
1095
1159
|
const importedClients = new Set<string>();
|
|
1096
1160
|
const pushClientImport = (c: SdkClientInfo): void => {
|
|
1097
1161
|
const key = `${c.className}|${c.importPath}`;
|
|
@@ -1099,41 +1163,14 @@ export function generateSdkAggregator(input: SdkAggregatorInput): string {
|
|
|
1099
1163
|
importedClients.add(key);
|
|
1100
1164
|
lines.push(`import { ${c.className} } from '${c.importPath}';`);
|
|
1101
1165
|
};
|
|
1166
|
+
// Areas first, then top-level — keeps the aggregator's import order stable.
|
|
1167
|
+
for (const area of input.areas) pushClientImport(area.client);
|
|
1102
1168
|
for (const c of input.topLevelClients) pushClientImport(c);
|
|
1103
|
-
for (const area of input.areas) {
|
|
1104
|
-
for (const sc of area.subareaClients) pushClientImport(sc.client);
|
|
1105
|
-
}
|
|
1106
1169
|
lines.push('');
|
|
1107
1170
|
|
|
1108
|
-
// ── <Area>Client classes ──
|
|
1109
|
-
for (const area of input.areas) {
|
|
1110
|
-
const areaClassName = deriveAreaClientClassName(area.area);
|
|
1111
|
-
const inlined = inlinedByArea.get(area.area)!;
|
|
1112
|
-
lines.push(`class ${areaClassName} {`);
|
|
1113
|
-
// Subarea property declarations
|
|
1114
|
-
for (const sc of area.subareaClients) {
|
|
1115
|
-
lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
|
|
1116
|
-
}
|
|
1117
|
-
if (area.subareaClients.length > 0) lines.push('');
|
|
1118
|
-
// Constructor
|
|
1119
|
-
if (inlined.lines.length > 0 || area.subareaClients.length > 0) {
|
|
1120
|
-
const fetchModifier = inlined.lines.length > 0 ? 'private ' : '';
|
|
1121
|
-
lines.push(` constructor(${fetchModifier}fetch: SdkFetch) {`);
|
|
1122
|
-
for (const sc of area.subareaClients) {
|
|
1123
|
-
lines.push(` this.${sc.propertyName} = new ${sc.client.className}(fetch);`);
|
|
1124
|
-
}
|
|
1125
|
-
lines.push(' }');
|
|
1126
|
-
}
|
|
1127
|
-
// Inlined methods (already class-body indented)
|
|
1128
|
-
for (const ln of inlined.lines) lines.push(ln);
|
|
1129
|
-
lines.push('}');
|
|
1130
|
-
lines.push('');
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// ── Sdk aggregator ──
|
|
1134
1171
|
lines.push(`export class ${sdkClassName} {`);
|
|
1135
1172
|
for (const area of input.areas) {
|
|
1136
|
-
lines.push(` readonly ${deriveAreaPropertyName(area.area)}: ${
|
|
1173
|
+
lines.push(` readonly ${deriveAreaPropertyName(area.area)}: ${area.client.className};`);
|
|
1137
1174
|
}
|
|
1138
1175
|
for (const c of input.topLevelClients) {
|
|
1139
1176
|
lines.push(` readonly ${c.propertyName}: ${c.className};`);
|
|
@@ -1142,7 +1179,7 @@ export function generateSdkAggregator(input: SdkAggregatorInput): string {
|
|
|
1142
1179
|
lines.push(' constructor(options: SdkOptions) {');
|
|
1143
1180
|
lines.push(' const sdkFetch = options.fetch ?? createSdkFetch(options);');
|
|
1144
1181
|
for (const area of input.areas) {
|
|
1145
|
-
lines.push(` this.${deriveAreaPropertyName(area.area)} = new ${
|
|
1182
|
+
lines.push(` this.${deriveAreaPropertyName(area.area)} = new ${area.client.className}(sdkFetch);`);
|
|
1146
1183
|
}
|
|
1147
1184
|
for (const c of input.topLevelClients) {
|
|
1148
1185
|
lines.push(` this.${c.propertyName} = new ${c.className}(sdkFetch);`);
|
package/src/index.ts
CHANGED
|
@@ -25,8 +25,11 @@ import {
|
|
|
25
25
|
generateSdk,
|
|
26
26
|
generateSdkOptions,
|
|
27
27
|
generateSdkAggregator,
|
|
28
|
+
generateAreaClient,
|
|
28
29
|
deriveClientClassName,
|
|
29
30
|
deriveClientPropertyName,
|
|
31
|
+
deriveAreaClientClassName,
|
|
32
|
+
deriveAreaPropertyName,
|
|
30
33
|
deriveSubareaClientClassName,
|
|
31
34
|
deriveSubareaPropertyName,
|
|
32
35
|
getAreaSubarea,
|
|
@@ -42,6 +45,7 @@ import {
|
|
|
42
45
|
computeOpOutPath,
|
|
43
46
|
computeContractOutPath,
|
|
44
47
|
computeSdkOutPath,
|
|
48
|
+
computeSdkAreaClientOutPath,
|
|
45
49
|
computeSdkTypeOutPath,
|
|
46
50
|
generateBarrelFiles,
|
|
47
51
|
computePubliclyReachableTypes,
|
|
@@ -548,12 +552,13 @@ function collectSdkOutput(
|
|
|
548
552
|
}
|
|
549
553
|
|
|
550
554
|
// ── Global files: sdk-options, aggregator, barrels, root index ──
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
555
|
+
// sdk-options.ts is a constant; the aggregator is small (just imports + a wrapper class)
|
|
556
|
+
// and depends on the cross-cutting client list, so it's cheap to regenerate every run.
|
|
557
|
+
// Per-area `<area>.client.ts` files are cached as their own units below.
|
|
554
558
|
globalFiles.push({ relativePath: sdkOptionsPath, content: generateSdkOptions() });
|
|
555
559
|
|
|
556
560
|
const hasAnything = sdkClientInfos.length > 0 || areaBuckets.size > 0;
|
|
561
|
+
const areaClientOutPaths = new Map<string, string>(); // area → absolute outPath of <area>.client.ts
|
|
557
562
|
if (hasAnything) {
|
|
558
563
|
const sdkEntryDir = dirname(sdkEntryPath);
|
|
559
564
|
const sdkOptionsRel = relative(sdkEntryDir, sdkOptionsPath).replace(/\.ts$/, '.js');
|
|
@@ -565,52 +570,104 @@ function collectSdkOutput(
|
|
|
565
570
|
.join('') + 'Sdk'
|
|
566
571
|
: 'Sdk';
|
|
567
572
|
|
|
568
|
-
const toClientImport = (info: { outPath: string; className: string; propertyName: string }): SdkClientInfo => {
|
|
569
|
-
let rel = relative(
|
|
573
|
+
const toClientImport = (sourceDir: string, info: { outPath: string; className: string; propertyName: string }): SdkClientInfo => {
|
|
574
|
+
let rel = relative(sourceDir, info.outPath).replace(/\.ts$/, '.js');
|
|
570
575
|
if (!rel.startsWith('.')) rel = './' + rel;
|
|
571
576
|
return { className: info.className, propertyName: info.propertyName, importPath: rel };
|
|
572
577
|
};
|
|
573
578
|
|
|
574
|
-
const topLevelClients: SdkClientInfo[] = topLevelEntries.map(e =>
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
subareaClients: bucket.leaves
|
|
600
|
-
.sort((a, b) => a.subarea.localeCompare(b.subarea))
|
|
601
|
-
.map(l => ({
|
|
579
|
+
const topLevelClients: SdkClientInfo[] = topLevelEntries.map(e =>
|
|
580
|
+
toClientImport(sdkEntryDir, {
|
|
581
|
+
outPath: e.outPath,
|
|
582
|
+
className: deriveClientClassName(e.ast.file),
|
|
583
|
+
propertyName: deriveClientPropertyName(e.ast.file),
|
|
584
|
+
}),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
// ── Per-area `<area>.client.ts` units ──
|
|
588
|
+
const areaInfos: SdkAreaInfo[] = [];
|
|
589
|
+
const sortedAreas = [...areaBuckets.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
590
|
+
for (const [area, bucket] of sortedAreas) {
|
|
591
|
+
const areaClientOutPath = computeSdkAreaClientOutPath(area, sdkBase, config.output!.clients);
|
|
592
|
+
areaClientOutPaths.set(area, areaClientOutPath);
|
|
593
|
+
const areaClassName = deriveAreaClientClassName(area);
|
|
594
|
+
const areaPropertyName = deriveAreaPropertyName(area);
|
|
595
|
+
sdkClientInfos.push({ outPath: areaClientOutPath, className: areaClassName, propertyName: areaPropertyName });
|
|
596
|
+
|
|
597
|
+
const subareaClients = bucket.leaves
|
|
598
|
+
.sort((a, b) => a.subarea.localeCompare(b.subarea))
|
|
599
|
+
.map(l => ({
|
|
600
|
+
propertyName: deriveSubareaPropertyName(l.subarea),
|
|
601
|
+
client: toClientImport(dirname(areaClientOutPath), {
|
|
602
|
+
outPath: l.outPath,
|
|
603
|
+
className: deriveSubareaClientClassName(area, l.subarea),
|
|
602
604
|
propertyName: deriveSubareaPropertyName(l.subarea),
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
605
|
+
}),
|
|
606
|
+
}));
|
|
607
|
+
|
|
608
|
+
// Fingerprint covers every input the area client depends on:
|
|
609
|
+
// - all inline roots (full AST)
|
|
610
|
+
// - subarea client metadata (className / propertyName / import path)
|
|
611
|
+
// - the modelOutPaths slice for refs across all inline roots
|
|
612
|
+
// - modelsWithInput/Output slices
|
|
613
|
+
const allInlineRefs = new Set<string>();
|
|
614
|
+
for (const r of bucket.inlineRoots) {
|
|
615
|
+
for (const ref of collectOpRootRefs(r, modelMap)) allInlineRefs.add(ref);
|
|
616
|
+
}
|
|
617
|
+
const fingerprint = hashFingerprint({
|
|
618
|
+
kind: 'sdk-area-client',
|
|
619
|
+
v: TYPESCRIPT_CODEGEN_VERSION,
|
|
620
|
+
outPath: areaClientOutPath,
|
|
621
|
+
area,
|
|
622
|
+
inlineRoots: bucket.inlineRoots,
|
|
623
|
+
subareaClients,
|
|
624
|
+
outPathSlice: sliceOutPathMap(allInlineRefs, sdkModelOutPaths, modelsWithInput, modelsWithOutput),
|
|
625
|
+
modelsWithInput: sliceModelSet(allInlineRefs, new Set(), modelsWithInput),
|
|
626
|
+
modelsWithOutput: sliceModelSet(allInlineRefs, new Set(), modelsWithOutput),
|
|
627
|
+
sdkOptionsPath,
|
|
628
|
+
includeInternal: config.includeInternal ?? false,
|
|
629
|
+
sub: subConfigKey,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const inlineFilesForGen = bucket.inlineRoots.map(root => ({
|
|
633
|
+
root,
|
|
634
|
+
codegenOptions: {
|
|
635
|
+
typeImportPathTemplate: undefined,
|
|
636
|
+
outPath: areaClientOutPath,
|
|
637
|
+
modelOutPaths: sdkModelOutPaths,
|
|
638
|
+
sdkOptionsPath,
|
|
639
|
+
modelsWithInput,
|
|
640
|
+
modelsWithOutput,
|
|
641
|
+
includeInternal: config.includeInternal,
|
|
642
|
+
},
|
|
609
643
|
}));
|
|
610
644
|
|
|
645
|
+
units.push({
|
|
646
|
+
key: `sdk-area-client::${areaClientOutPath}`,
|
|
647
|
+
fingerprint,
|
|
648
|
+
render: () => [
|
|
649
|
+
{
|
|
650
|
+
relativePath: areaClientOutPath,
|
|
651
|
+
content: generateAreaClient({
|
|
652
|
+
area,
|
|
653
|
+
outPath: areaClientOutPath,
|
|
654
|
+
inlineFiles: inlineFilesForGen,
|
|
655
|
+
subareaClients,
|
|
656
|
+
sdkOptionsPath,
|
|
657
|
+
}),
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
areaInfos.push({
|
|
663
|
+
area,
|
|
664
|
+
client: toClientImport(sdkEntryDir, { outPath: areaClientOutPath, className: areaClassName, propertyName: areaPropertyName }),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
611
668
|
globalFiles.push({
|
|
612
669
|
relativePath: sdkEntryPath,
|
|
613
|
-
content: generateSdkAggregator({ topLevelClients, areas, sdkOptionsImportPath, sdkClassName }),
|
|
670
|
+
content: generateSdkAggregator({ topLevelClients, areas: areaInfos, sdkOptionsImportPath, sdkClassName }),
|
|
614
671
|
});
|
|
615
672
|
}
|
|
616
673
|
|
package/src/path-utils.ts
CHANGED
|
@@ -96,6 +96,38 @@ export function computeSdkOutPath(
|
|
|
96
96
|
return join(baseOutDir, relDir, defaultOutName);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the output path for a synthesized `<area>.client.ts` — the file holding the
|
|
101
|
+
* `<Area>Client` class that aggregates an area's inlined methods and subarea wiring.
|
|
102
|
+
*
|
|
103
|
+
* Uses the same `output.clients` template as leaf clients, with `{filename}` and `{area}`
|
|
104
|
+
* substituted to the area name and `{subarea}` substituted to the empty string. Resulting
|
|
105
|
+
* double-slashes from the empty substitution are collapsed, and a final segment that
|
|
106
|
+
* would otherwise begin with a dot (e.g. `.client.ts` from `{subarea}.client.ts`) is
|
|
107
|
+
* prefixed with the area so the file isn't hidden.
|
|
108
|
+
*/
|
|
109
|
+
export function computeSdkAreaClientOutPath(area: string, rootDir: string, clientOutput: string | undefined): string {
|
|
110
|
+
const filename = area;
|
|
111
|
+
const baseOutDir = resolve(rootDir);
|
|
112
|
+
const fixHiddenSegment = (path: string): string => {
|
|
113
|
+
const segments = path.split('/');
|
|
114
|
+
const last = segments[segments.length - 1] ?? '';
|
|
115
|
+
if (last.startsWith('.')) segments[segments.length - 1] = `${filename}${last}`;
|
|
116
|
+
return segments.join('/');
|
|
117
|
+
};
|
|
118
|
+
if (clientOutput && TEMPLATE_VAR_RE.test(clientOutput)) {
|
|
119
|
+
const resolved = resolveTemplate(clientOutput, { filename, dir: '', ext: 'ck', area, subarea: '' });
|
|
120
|
+
const cleaned = fixHiddenSegment(resolved.replace(/\/+/g, '/').replace(/^\//, ''));
|
|
121
|
+
if (includesFilename(cleaned)) return join(baseOutDir, cleaned);
|
|
122
|
+
return join(baseOutDir, cleaned, `${filename}.client.ts`);
|
|
123
|
+
}
|
|
124
|
+
if (clientOutput) {
|
|
125
|
+
if (includesFilename(clientOutput)) return join(baseOutDir, clientOutput);
|
|
126
|
+
return join(baseOutDir, clientOutput, `${filename}.client.ts`);
|
|
127
|
+
}
|
|
128
|
+
return join(baseOutDir, `${filename}.client.ts`);
|
|
129
|
+
}
|
|
130
|
+
|
|
99
131
|
export function computeSdkTypeOutPath(
|
|
100
132
|
filePath: string,
|
|
101
133
|
rootDir: string,
|