@workos/oagen-emitters 0.16.0 → 0.16.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{plugin-DuB1UozS.mjs → plugin-CpO8rePT.mjs} +1164 -490
- package/dist/plugin-CpO8rePT.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +7 -7
- package/src/node/enums.ts +17 -4
- package/src/node/index.ts +264 -4
- package/src/node/live-surface.ts +309 -0
- package/src/node/models.ts +69 -3
- package/src/node/naming.ts +204 -23
- package/src/node/resources.ts +39 -3
- package/src/node/utils.ts +140 -22
- package/test/node/enums.test.ts +239 -2
- package/test/node/live-surface.test.ts +771 -1
- package/test/node/models.test.ts +738 -3
- package/test/node/naming.test.ts +159 -0
- package/test/node/resources.test.ts +464 -0
- package/test/node/utils.test.ts +157 -2
- package/dist/plugin-DuB1UozS.mjs.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { assignModelsToServices, assignModelsToServices as assignModelsToServices$1, collectEnumRefs, collectFieldDependencies, collectFieldDependencies as collectFieldDependencies$1, collectModelRefs, mapTypeRef, planOperation, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toUpperSnakeCase, walkTypeRef } from "@workos/oagen";
|
|
1
2
|
import * as fs$1 from "node:fs";
|
|
2
3
|
import fs, { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import * as path$1 from "node:path";
|
|
4
5
|
import path, { resolve } from "node:path";
|
|
5
|
-
import { assignModelsToServices, assignModelsToServices as assignModelsToServices$1, collectEnumRefs, collectFieldDependencies, collectFieldDependencies as collectFieldDependencies$1, collectModelRefs, mapTypeRef, planOperation, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toUpperSnakeCase, walkTypeRef } from "@workos/oagen";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { dotnetExtractor, elixirExtractor, goExtractor, kotlinExtractor, nodeExtractor, phpExtractor, pythonExtractor, rubyExtractor, rustExtractor } from "@workos/oagen/compat";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
@@ -2844,6 +2844,25 @@ function setBaselineInterfaceNames(names) {
|
|
|
2844
2844
|
baselineInterfaceNames = names;
|
|
2845
2845
|
}
|
|
2846
2846
|
/**
|
|
2847
|
+
* Every name DECLARED by the live SDK or baseline api-surface — interfaces
|
|
2848
|
+
* AND type aliases. Exact-name declarations preempt structural renames in
|
|
2849
|
+
* `resolveInterfaceName`: when the IR model's own name is already declared,
|
|
2850
|
+
* the structural matcher must not re-point it at a different declaration.
|
|
2851
|
+
*
|
|
2852
|
+
* This matters for alias-form files (`export type X = Y;`): the engine's
|
|
2853
|
+
* api-surface records X under `typeAliases` with no fields, so its
|
|
2854
|
+
* exact-name pass cannot claim X and the Jaccard fallback "renames" IR
|
|
2855
|
+
* model X to whatever interface looks similar. Propagating that rename
|
|
2856
|
+
* emitted duplicate, renamed declarations whose form flip-flopped on every
|
|
2857
|
+
* regeneration (workos-node ApiKeyOwner / UserManagement model files).
|
|
2858
|
+
*
|
|
2859
|
+
* Set by `index.ts` immediately after `getSurface(ctx)` runs.
|
|
2860
|
+
*/
|
|
2861
|
+
let baselineDeclaredNames = /* @__PURE__ */ new Set();
|
|
2862
|
+
function setBaselineDeclaredNames(names) {
|
|
2863
|
+
baselineDeclaredNames = names;
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2847
2866
|
* IR models that belong to newly-adopted services should not be renamed by
|
|
2848
2867
|
* structural baseline matches from unrelated hand-written services.
|
|
2849
2868
|
*/
|
|
@@ -2880,6 +2899,142 @@ function setStructurallyRenamedDomainNames(names) {
|
|
|
2880
2899
|
structurallyRenamedDomainNames = names;
|
|
2881
2900
|
}
|
|
2882
2901
|
/**
|
|
2902
|
+
* The structural half of `resolveInterfaceName`, pre-injectivity: look up the
|
|
2903
|
+
* engine's structurally-inferred match (`overlayLookup.modelNameByIR`), apply
|
|
2904
|
+
* the adopted/discriminated/declared-name guards, and normalize legacy
|
|
2905
|
+
* `Serialized*` / wire-shaped `*Response` matches down to the baseline domain
|
|
2906
|
+
* name. Returns the candidate live name, or undefined when no structural
|
|
2907
|
+
* match applies. Shared by `resolveInterfaceName` and the claims registry so
|
|
2908
|
+
* both see the exact same candidate for every IR model.
|
|
2909
|
+
*/
|
|
2910
|
+
function inferStructuralRename(name, ctx) {
|
|
2911
|
+
let inferred = adoptedModelNames.has(name) || discriminatedModelNames.has(name) ? void 0 : ctx.overlayLookup?.modelNameByIR?.get(name);
|
|
2912
|
+
if (inferred && inferred !== name && baselineDeclaredNames.has(name)) return;
|
|
2913
|
+
if (!inferred) return void 0;
|
|
2914
|
+
if (inferred.startsWith("Serialized")) {
|
|
2915
|
+
const stripped = inferred.slice(10);
|
|
2916
|
+
if (stripped && ctx.apiSurface?.interfaces?.[stripped]) inferred = stripped;
|
|
2917
|
+
}
|
|
2918
|
+
if (inferred.endsWith("Response") && ctx.apiSurface?.interfaces) {
|
|
2919
|
+
const stripped = inferred.slice(0, -8);
|
|
2920
|
+
if (stripped && ctx.apiSurface.interfaces[stripped]) inferred = stripped;
|
|
2921
|
+
}
|
|
2922
|
+
return inferred;
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Per-run registry making structural name resolution INJECTIVE: live-surface
|
|
2926
|
+
* name → the single IR model name allowed to claim it. Built lazily from
|
|
2927
|
+
* `ctx.spec.models` on first use and cached per ctx.
|
|
2928
|
+
*
|
|
2929
|
+
* Cache-correctness invariant (relied on, not assumed): the `ctx` reaching
|
|
2930
|
+
* this function is always the memoized `nodeCtx` from `withNodeOperationOverrides`
|
|
2931
|
+
* — every emitter hook derives it as its first step and threads it through
|
|
2932
|
+
* `getSurface`/`resolveInterfaceName`, and that helper returns one stable
|
|
2933
|
+
* object per run. `nodeCtx.spec` is built once via spread and `spec.models` is
|
|
2934
|
+
* never reassigned or mutated in place anywhere (enrichment pushes only onto a
|
|
2935
|
+
* pre-enrichment local collector). So the cached value can never drift from
|
|
2936
|
+
* the `spec.models` it was computed from. Do not begin mutating `spec.models`
|
|
2937
|
+
* under a live ctx without invalidating this cache.
|
|
2938
|
+
*
|
|
2939
|
+
* Without it, the structural fallback could map two distinct IR models onto
|
|
2940
|
+
* one live declaration. workos-node AuditLogs evidence: IR
|
|
2941
|
+
* `AuditLogEventActor` and `AuditLogEventTarget` (near-identical shapes) both
|
|
2942
|
+
* resolved to the hand-written `AuditLogActor`, so
|
|
2943
|
+
* `audit-log-event-target.interface.ts` was emitted declaring
|
|
2944
|
+
* `export interface AuditLogActor` (file stem and declaration disagree),
|
|
2945
|
+
* with duplicate imports/`describe` blocks and two `serializeAuditLogActor`
|
|
2946
|
+
* definitions downstream. The raw engine overlay is injective on its own
|
|
2947
|
+
* names, but the resolver's `Serialized*`/`*Response` normalization below can
|
|
2948
|
+
* collapse two distinct raw matches onto one bare name — the claims registry
|
|
2949
|
+
* gates the final, post-normalization answer.
|
|
2950
|
+
*
|
|
2951
|
+
* Claim order:
|
|
2952
|
+
* 1. Exact-name overrides (`overlayLookup.interfaceByName`) claim first.
|
|
2953
|
+
* 2. Identity structural matches (IR name === live name) claim their own name.
|
|
2954
|
+
* 3. Contested renames go to the claimant with the higher field-overlap
|
|
2955
|
+
* similarity; ties break toward the closer name (edit distance), then
|
|
2956
|
+
* lexicographically for determinism. Losers are NEVER unified — they keep
|
|
2957
|
+
* their canonical IR-derived names.
|
|
2958
|
+
*/
|
|
2959
|
+
const structuralClaimsCache = /* @__PURE__ */ new WeakMap();
|
|
2960
|
+
/** Jaccard similarity between two normalized field-name sets. */
|
|
2961
|
+
function fieldJaccard(a, b) {
|
|
2962
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
2963
|
+
let intersection = 0;
|
|
2964
|
+
for (const item of a) if (b.has(item)) intersection++;
|
|
2965
|
+
const union = a.size + b.size - intersection;
|
|
2966
|
+
return union === 0 ? 0 : intersection / union;
|
|
2967
|
+
}
|
|
2968
|
+
/** Levenshtein distance over lowercased names (tie-break for contested claims). */
|
|
2969
|
+
function nameDistance(a, b) {
|
|
2970
|
+
const s = a.toLowerCase();
|
|
2971
|
+
const t = b.toLowerCase();
|
|
2972
|
+
if (s === t) return 0;
|
|
2973
|
+
let prev = Array.from({ length: t.length + 1 }, (_, i) => i);
|
|
2974
|
+
for (let i = 1; i <= s.length; i++) {
|
|
2975
|
+
const curr = [i];
|
|
2976
|
+
for (let j = 1; j <= t.length; j++) {
|
|
2977
|
+
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
2978
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
2979
|
+
}
|
|
2980
|
+
prev = curr;
|
|
2981
|
+
}
|
|
2982
|
+
return prev[t.length];
|
|
2983
|
+
}
|
|
2984
|
+
/** Normalized field-name signature of an IR model. */
|
|
2985
|
+
function irFieldSignature(model) {
|
|
2986
|
+
return new Set(model.fields.map((f) => toSnakeCase(f.name)));
|
|
2987
|
+
}
|
|
2988
|
+
/** Normalized field-name signature of a live-surface interface, if known. */
|
|
2989
|
+
function liveFieldSignature(ctx, liveName) {
|
|
2990
|
+
const iface = ctx.apiSurface?.interfaces?.[liveName];
|
|
2991
|
+
if (!iface?.fields) return void 0;
|
|
2992
|
+
return new Set(Object.keys(iface.fields).map((f) => toSnakeCase(f)));
|
|
2993
|
+
}
|
|
2994
|
+
function getStructuralNameClaims(ctx) {
|
|
2995
|
+
const cached = structuralClaimsCache.get(ctx);
|
|
2996
|
+
if (cached) return cached;
|
|
2997
|
+
const claims = /* @__PURE__ */ new Map();
|
|
2998
|
+
for (const [irName, liveName] of ctx.overlayLookup?.interfaceByName ?? /* @__PURE__ */ new Map()) if (!claims.has(liveName)) claims.set(liveName, irName);
|
|
2999
|
+
const contested = /* @__PURE__ */ new Map();
|
|
3000
|
+
const modelsByName = /* @__PURE__ */ new Map();
|
|
3001
|
+
for (const model of ctx.spec.models) {
|
|
3002
|
+
modelsByName.set(model.name, model);
|
|
3003
|
+
if (ctx.overlayLookup?.interfaceByName?.has(model.name)) continue;
|
|
3004
|
+
const inferred = inferStructuralRename(model.name, ctx);
|
|
3005
|
+
if (!inferred) continue;
|
|
3006
|
+
if (inferred === model.name) {
|
|
3007
|
+
if (!claims.has(inferred)) claims.set(inferred, model.name);
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
const group = contested.get(inferred);
|
|
3011
|
+
if (group) group.push(model.name);
|
|
3012
|
+
else contested.set(inferred, [model.name]);
|
|
3013
|
+
}
|
|
3014
|
+
for (const [liveName, irNames] of contested) {
|
|
3015
|
+
if (claims.has(liveName)) continue;
|
|
3016
|
+
let winner = irNames[0];
|
|
3017
|
+
if (irNames.length > 1) {
|
|
3018
|
+
const liveFields = liveFieldSignature(ctx, liveName);
|
|
3019
|
+
const scoreOf = (irName) => {
|
|
3020
|
+
const model = modelsByName.get(irName);
|
|
3021
|
+
if (!model || !liveFields) return 0;
|
|
3022
|
+
return fieldJaccard(irFieldSignature(model), liveFields);
|
|
3023
|
+
};
|
|
3024
|
+
winner = [...irNames].sort((a, b) => {
|
|
3025
|
+
const scoreDiff = scoreOf(b) - scoreOf(a);
|
|
3026
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
3027
|
+
const distDiff = nameDistance(a, liveName) - nameDistance(b, liveName);
|
|
3028
|
+
if (distDiff !== 0) return distDiff;
|
|
3029
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
3030
|
+
})[0];
|
|
3031
|
+
}
|
|
3032
|
+
claims.set(liveName, winner);
|
|
3033
|
+
}
|
|
3034
|
+
structuralClaimsCache.set(ctx, claims);
|
|
3035
|
+
return claims;
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
2883
3038
|
* Wire/response interface name.
|
|
2884
3039
|
*
|
|
2885
3040
|
* Resolution order:
|
|
@@ -2978,7 +3133,8 @@ function resolveClassName$5(service, ctx) {
|
|
|
2978
3133
|
* 1. `overlayLookup.interfaceByName` — exact-name overrides from the live SDK.
|
|
2979
3134
|
* 2. `overlayLookup.modelNameByIR` — structurally-inferred matches (e.g., IR
|
|
2980
3135
|
* `ValidateApiKey` with one field `value: string` → live SDK interface
|
|
2981
|
-
* `ValidateApiKeyOptions`)
|
|
3136
|
+
* `ValidateApiKeyOptions`), gated by the injective claims registry: each
|
|
3137
|
+
* live name goes to at most one IR model (see `getStructuralNameClaims`).
|
|
2982
3138
|
* 3. Type-alias resolution (when an alias points to an interface).
|
|
2983
3139
|
* 4. Suffix-fallback heuristic for the workos-node `*Options` convention:
|
|
2984
3140
|
* when the IR name `X` has no baseline match but `XOptions` does, use
|
|
@@ -2989,17 +3145,13 @@ function resolveClassName$5(service, ctx) {
|
|
|
2989
3145
|
function resolveInterfaceName(name, ctx, opts) {
|
|
2990
3146
|
const existing = ctx.overlayLookup?.interfaceByName?.get(name);
|
|
2991
3147
|
if (existing) return existing;
|
|
2992
|
-
let inferred =
|
|
2993
|
-
if (inferred) {
|
|
2994
|
-
if (inferred
|
|
2995
|
-
const
|
|
2996
|
-
if (
|
|
2997
|
-
}
|
|
2998
|
-
if (inferred.endsWith("Response") && ctx.apiSurface?.interfaces) {
|
|
2999
|
-
const stripped = inferred.slice(0, -8);
|
|
3000
|
-
if (stripped && ctx.apiSurface.interfaces[stripped]) inferred = stripped;
|
|
3148
|
+
let inferred = inferStructuralRename(name, ctx);
|
|
3149
|
+
if (inferred !== void 0) {
|
|
3150
|
+
if (inferred !== name) {
|
|
3151
|
+
const claimant = getStructuralNameClaims(ctx).get(inferred);
|
|
3152
|
+
if (claimant !== void 0 && claimant !== name) inferred = void 0;
|
|
3001
3153
|
}
|
|
3002
|
-
return inferred;
|
|
3154
|
+
if (inferred !== void 0) return inferred;
|
|
3003
3155
|
}
|
|
3004
3156
|
if (!opts?.skipTypeAlias && ctx.apiSurface?.typeAliases) {
|
|
3005
3157
|
const alias = ctx.apiSurface.typeAliases[name];
|
|
@@ -3120,416 +3272,84 @@ function parenthesizeUnion$1(type) {
|
|
|
3120
3272
|
return type.includes(" | ") || type.includes(" & ") ? `(${type})` : type;
|
|
3121
3273
|
}
|
|
3122
3274
|
//#endregion
|
|
3123
|
-
//#region src/node/
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
*/
|
|
3127
|
-
function relativeImport(fromFile, toFile) {
|
|
3128
|
-
const fromDir = fromFile.split("/").slice(0, -1);
|
|
3129
|
-
const toFileParts = toFile.split("/");
|
|
3130
|
-
const toDir = toFileParts.slice(0, -1);
|
|
3131
|
-
const toFileName = toFileParts[toFileParts.length - 1];
|
|
3132
|
-
let common = 0;
|
|
3133
|
-
while (common < fromDir.length && common < toDir.length && fromDir[common] === toDir[common]) common++;
|
|
3134
|
-
const ups = fromDir.length - common;
|
|
3135
|
-
const downs = toDir.slice(common);
|
|
3136
|
-
let result = [
|
|
3137
|
-
...Array(ups).fill(".."),
|
|
3138
|
-
...downs,
|
|
3139
|
-
toFileName
|
|
3140
|
-
].join("/");
|
|
3141
|
-
result = result.replace(/\.ts$/, "");
|
|
3142
|
-
if (!result.startsWith(".")) result = "./" + result;
|
|
3143
|
-
return result;
|
|
3275
|
+
//#region src/node/options.ts
|
|
3276
|
+
function nodeOptions(ctx) {
|
|
3277
|
+
return ctx.emitterOptions ?? {};
|
|
3144
3278
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
*/
|
|
3148
|
-
function docComment$2(description, indent = 0) {
|
|
3149
|
-
const pad = " ".repeat(indent);
|
|
3150
|
-
const descLines = description.split("\n");
|
|
3151
|
-
if (descLines.length === 1) return [`${pad}/** ${descLines[0]} */`];
|
|
3152
|
-
const lines = [`${pad}/**`];
|
|
3153
|
-
for (const line of descLines) lines.push(line === "" ? `${pad} *` : `${pad} * ${line}`);
|
|
3154
|
-
lines.push(`${pad} */`);
|
|
3155
|
-
return lines;
|
|
3279
|
+
function normalizeServiceName(name) {
|
|
3280
|
+
return name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
|
3156
3281
|
}
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
const result = /* @__PURE__ */ new Map();
|
|
3162
|
-
for (const model of models) {
|
|
3163
|
-
if (!model.typeParams?.length) continue;
|
|
3164
|
-
const defaults = model.typeParams.map((tp) => tp.default ? mapTypeRef$7(tp.default) : "unknown");
|
|
3165
|
-
result.set(model.name, `<${defaults.join(", ")}>`);
|
|
3166
|
-
}
|
|
3167
|
-
return result;
|
|
3282
|
+
function ownedLookupNames(name) {
|
|
3283
|
+
const names = [name];
|
|
3284
|
+
if (name.startsWith("__baseline_dir__:")) names.push(name.slice(17));
|
|
3285
|
+
return names;
|
|
3168
3286
|
}
|
|
3287
|
+
function isNodeOwnedService(ctx, ...names) {
|
|
3288
|
+
const configured = nodeOptions(ctx).ownedServices ?? [];
|
|
3289
|
+
if (configured.length === 0) return false;
|
|
3290
|
+
const owned = new Set(configured.map(normalizeServiceName));
|
|
3291
|
+
return names.some((name) => name !== void 0 ? ownedLookupNames(name).some((candidate) => owned.has(normalizeServiceName(candidate))) : false);
|
|
3292
|
+
}
|
|
3293
|
+
//#endregion
|
|
3294
|
+
//#region src/node/live-surface.ts
|
|
3295
|
+
const SRC_DIR = "src";
|
|
3169
3296
|
/**
|
|
3170
|
-
*
|
|
3297
|
+
* Walk `${rootDir}/src/` and build a live-surface snapshot.
|
|
3298
|
+
*
|
|
3299
|
+
* If `${rootDir}/src/` does not exist, returns an empty surface (greenfield).
|
|
3300
|
+
* Reads each `.ts` file once; ignores `.spec.ts` and `.test.ts`.
|
|
3171
3301
|
*/
|
|
3172
|
-
function
|
|
3173
|
-
const
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
if (!
|
|
3190
|
-
|
|
3302
|
+
function buildLiveSurface(rootDir) {
|
|
3303
|
+
const surface = {
|
|
3304
|
+
rootDir,
|
|
3305
|
+
files: /* @__PURE__ */ new Set(),
|
|
3306
|
+
trackedFiles: loadGitTrackedFiles(rootDir),
|
|
3307
|
+
protectedFiles: /* @__PURE__ */ new Set(),
|
|
3308
|
+
autogenFiles: /* @__PURE__ */ new Set(),
|
|
3309
|
+
classes: /* @__PURE__ */ new Map(),
|
|
3310
|
+
interfaces: /* @__PURE__ */ new Map(),
|
|
3311
|
+
functions: /* @__PURE__ */ new Map(),
|
|
3312
|
+
constObjectEnums: /* @__PURE__ */ new Map()
|
|
3313
|
+
};
|
|
3314
|
+
const srcRoot = path$1.join(rootDir, SRC_DIR);
|
|
3315
|
+
if (!fs$1.existsSync(srcRoot) || !fs$1.statSync(srcRoot).isDirectory()) return surface;
|
|
3316
|
+
for (const absPath of walk$1(srcRoot)) {
|
|
3317
|
+
const rel = toPosix(path$1.relative(rootDir, absPath));
|
|
3318
|
+
surface.files.add(rel);
|
|
3319
|
+
if (!rel.endsWith(".ts")) continue;
|
|
3320
|
+
if (surface.trackedFiles.size > 0 && !surface.trackedFiles.has(rel)) continue;
|
|
3321
|
+
let text;
|
|
3322
|
+
try {
|
|
3323
|
+
text = fs$1.readFileSync(absPath, "utf8");
|
|
3324
|
+
} catch {
|
|
3191
3325
|
continue;
|
|
3192
3326
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
if (usedNames.length === 0) continue;
|
|
3198
|
-
if (usedNames.length === names.length) kept.push(line);
|
|
3199
|
-
else {
|
|
3200
|
-
const isTypeImport = line.startsWith("import type");
|
|
3201
|
-
const fromMatch = line.match(/from\s+['"]([^'"]+)['"]/);
|
|
3202
|
-
if (fromMatch) {
|
|
3203
|
-
const prefix = isTypeImport ? "import type" : "import";
|
|
3204
|
-
kept.push(`${prefix} { ${usedNames.join(", ")} } from '${fromMatch[1]}';`);
|
|
3205
|
-
} else kept.push(line);
|
|
3206
|
-
}
|
|
3327
|
+
if (isProtected(text)) surface.protectedFiles.add(rel);
|
|
3328
|
+
else if (isAutogen(text)) surface.autogenFiles.add(rel);
|
|
3329
|
+
if (rel.endsWith(".spec.ts") || rel.endsWith(".test.ts")) continue;
|
|
3330
|
+
extractDeclarations(text, rel, surface);
|
|
3207
3331
|
}
|
|
3208
|
-
return
|
|
3332
|
+
return surface;
|
|
3209
3333
|
}
|
|
3210
|
-
/** Built-in TypeScript types that are always available. */
|
|
3211
|
-
const TS_BUILTINS = new Set([
|
|
3212
|
-
"Record",
|
|
3213
|
-
"Promise",
|
|
3214
|
-
"Array",
|
|
3215
|
-
"Map",
|
|
3216
|
-
"Set",
|
|
3217
|
-
"Date",
|
|
3218
|
-
"string",
|
|
3219
|
-
"number",
|
|
3220
|
-
"boolean",
|
|
3221
|
-
"void",
|
|
3222
|
-
"null",
|
|
3223
|
-
"undefined",
|
|
3224
|
-
"any",
|
|
3225
|
-
"never",
|
|
3226
|
-
"unknown",
|
|
3227
|
-
"true",
|
|
3228
|
-
"false"
|
|
3229
|
-
]);
|
|
3230
3334
|
/**
|
|
3231
|
-
*
|
|
3335
|
+
* Module-level snapshot of the active live-surface, set by `index.ts` once per
|
|
3336
|
+
* generation run. Used by callers that don't have a clean way to thread
|
|
3337
|
+
* `EmitterContext` through (e.g. deeply-nested helpers in `resources.ts`
|
|
3338
|
+
* that decide whether to inline a serializer call).
|
|
3232
3339
|
*/
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
for (const e of ctx.spec.enums) knownNames.add(e.name);
|
|
3237
|
-
if (ctx.apiSurface?.interfaces) for (const name of Object.keys(ctx.apiSurface.interfaces)) knownNames.add(name);
|
|
3238
|
-
if (ctx.apiSurface?.typeAliases) for (const name of Object.keys(ctx.apiSurface.typeAliases)) knownNames.add(name);
|
|
3239
|
-
if (ctx.apiSurface?.enums) for (const name of Object.keys(ctx.apiSurface.enums)) knownNames.add(name);
|
|
3240
|
-
return knownNames;
|
|
3340
|
+
let activeSurface = null;
|
|
3341
|
+
function setActiveLiveSurface(surface) {
|
|
3342
|
+
activeSurface = surface;
|
|
3241
3343
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
*
|
|
3245
|
-
* When `ctx.apiSurface` is populated, the baseline `sourceFile` of an
|
|
3246
|
-
* existing interface wins over the IR-derived first-reference assignment.
|
|
3247
|
-
* This keeps generated imports pointing at the existing live SDK location
|
|
3248
|
-
* instead of duplicating a model into a different service directory.
|
|
3249
|
-
*/
|
|
3250
|
-
function createServiceDirResolver(models, services, ctx) {
|
|
3251
|
-
const modelToService = assignModelsToServices(models, services, ctx.modelHints);
|
|
3252
|
-
const serviceNameMap = buildServiceNameMap(services, ctx);
|
|
3253
|
-
const baselineDirByModel = /* @__PURE__ */ new Map();
|
|
3254
|
-
const recordSource = (name, info) => {
|
|
3255
|
-
const sourceFile = info?.sourceFile;
|
|
3256
|
-
if (!sourceFile) return;
|
|
3257
|
-
const m = sourceFile.match(/^src\/([^/]+)\//);
|
|
3258
|
-
if (!m) return;
|
|
3259
|
-
baselineDirByModel.set(name, m[1]);
|
|
3260
|
-
};
|
|
3261
|
-
for (const [name, info] of Object.entries(ctx.apiSurface?.interfaces ?? {})) recordSource(name, info);
|
|
3262
|
-
for (const [name, info] of Object.entries(ctx.apiSurface?.typeAliases ?? {})) if (!baselineDirByModel.has(name)) recordSource(name, info);
|
|
3263
|
-
for (const [modelName] of modelToService) {
|
|
3264
|
-
const dir = baselineDirByModel.get(modelName);
|
|
3265
|
-
if (!dir) continue;
|
|
3266
|
-
const synthetic = `__baseline_dir__:${dir}`;
|
|
3267
|
-
modelToService.set(modelName, synthetic);
|
|
3268
|
-
if (!serviceNameMap.has(synthetic)) serviceNameMap.set(synthetic, dir);
|
|
3269
|
-
}
|
|
3270
|
-
const resolveDir = (irService) => {
|
|
3271
|
-
if (!irService) return "common";
|
|
3272
|
-
if (irService.startsWith("__baseline_dir__:")) return irService.slice(17);
|
|
3273
|
-
return resolveServiceDir$1(serviceNameMap.get(irService) ?? irService);
|
|
3274
|
-
};
|
|
3275
|
-
return {
|
|
3276
|
-
modelToService,
|
|
3277
|
-
serviceNameMap,
|
|
3278
|
-
resolveDir
|
|
3279
|
-
};
|
|
3344
|
+
function liveSurfaceHasFunction(name) {
|
|
3345
|
+
return activeSurface?.functions.has(name) ?? false;
|
|
3280
3346
|
}
|
|
3281
|
-
/**
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
* Heuristic: strip string literals first (so `'GoogleSAML'` is not mistaken
|
|
3285
|
-
* for a type name), then look for any PascalCase token that isn't a known
|
|
3286
|
-
* type — those indicate an unbound generic parameter like `TCustomAttributes`.
|
|
3287
|
-
*/
|
|
3288
|
-
function isBaselineGeneric(fields, knownNames) {
|
|
3289
|
-
for (const [, bf] of Object.entries(fields)) {
|
|
3290
|
-
const typeNames = bf.type.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "").match(/\b[A-Z][a-zA-Z0-9]*\b/g);
|
|
3291
|
-
if (!typeNames) continue;
|
|
3292
|
-
for (const tn of typeNames) {
|
|
3293
|
-
if (TS_BUILTINS.has(tn)) continue;
|
|
3294
|
-
if (knownNames.has(tn)) continue;
|
|
3295
|
-
return true;
|
|
3296
|
-
}
|
|
3297
|
-
}
|
|
3298
|
-
return false;
|
|
3347
|
+
/** Returns the relative file path containing the function, if known. */
|
|
3348
|
+
function liveSurfaceFunctionPath(name) {
|
|
3349
|
+
return activeSurface?.functions.get(name);
|
|
3299
3350
|
}
|
|
3300
|
-
function
|
|
3301
|
-
return
|
|
3302
|
-
}
|
|
3303
|
-
/**
|
|
3304
|
-
* Find structurally identical models and build a deduplication map.
|
|
3305
|
-
*/
|
|
3306
|
-
function buildDeduplicationMap(models, ctx, reachable) {
|
|
3307
|
-
const dedup = /* @__PURE__ */ new Map();
|
|
3308
|
-
const fingerprints = /* @__PURE__ */ new Map();
|
|
3309
|
-
for (const model of models) {
|
|
3310
|
-
if (model.fields.length === 0) continue;
|
|
3311
|
-
const fp = modelFingerprint(model);
|
|
3312
|
-
const existing = fingerprints.get(fp);
|
|
3313
|
-
if (existing) if (reachable && !reachable.has(existing) && reachable.has(model.name)) {
|
|
3314
|
-
dedup.delete(existing);
|
|
3315
|
-
dedup.set(existing, model.name);
|
|
3316
|
-
fingerprints.set(fp, model.name);
|
|
3317
|
-
} else dedup.set(model.name, existing);
|
|
3318
|
-
else fingerprints.set(fp, model.name);
|
|
3319
|
-
}
|
|
3320
|
-
if (ctx) {
|
|
3321
|
-
const byDomainName = /* @__PURE__ */ new Map();
|
|
3322
|
-
for (const model of models) {
|
|
3323
|
-
if (model.fields.length === 0) continue;
|
|
3324
|
-
if (dedup.has(model.name)) continue;
|
|
3325
|
-
const domainName = resolveInterfaceName(model.name, ctx);
|
|
3326
|
-
const group = byDomainName.get(domainName);
|
|
3327
|
-
if (group) group.push(model);
|
|
3328
|
-
else byDomainName.set(domainName, [model]);
|
|
3329
|
-
}
|
|
3330
|
-
for (const [, group] of byDomainName) {
|
|
3331
|
-
if (group.length < 2) continue;
|
|
3332
|
-
group.sort((a, b) => {
|
|
3333
|
-
if (reachable) {
|
|
3334
|
-
const aReach = reachable.has(a.name) ? 0 : 1;
|
|
3335
|
-
const bReach = reachable.has(b.name) ? 0 : 1;
|
|
3336
|
-
if (aReach !== bReach) return aReach - bReach;
|
|
3337
|
-
}
|
|
3338
|
-
return b.fields.length - a.fields.length || a.name.localeCompare(b.name);
|
|
3339
|
-
});
|
|
3340
|
-
const canonical = group[0];
|
|
3341
|
-
for (let i = 1; i < group.length; i++) dedup.set(group[i].name, canonical.name);
|
|
3342
|
-
}
|
|
3343
|
-
}
|
|
3344
|
-
return dedup;
|
|
3345
|
-
}
|
|
3346
|
-
/**
|
|
3347
|
-
* Check whether a service's endpoints are already fully covered by existing
|
|
3348
|
-
* hand-written service classes.
|
|
3349
|
-
*/
|
|
3350
|
-
function isServiceCoveredByExisting(service, ctx) {
|
|
3351
|
-
if (getMountTarget(service, ctx) !== toPascalCase(service.name)) return true;
|
|
3352
|
-
const overlay = ctx.overlayLookup?.methodByOperation;
|
|
3353
|
-
if (!overlay || overlay.size === 0) return false;
|
|
3354
|
-
if (service.operations.length === 0) return false;
|
|
3355
|
-
const baselineClasses = ctx.apiSurface?.classes;
|
|
3356
|
-
if (!baselineClasses) return false;
|
|
3357
|
-
const existingClassNames = new Set(Object.keys(baselineClasses));
|
|
3358
|
-
return service.operations.every((op) => {
|
|
3359
|
-
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
3360
|
-
const match = overlay.get(httpKey);
|
|
3361
|
-
if (!match) return false;
|
|
3362
|
-
return existingClassNames.has(match.className);
|
|
3363
|
-
});
|
|
3364
|
-
}
|
|
3365
|
-
/**
|
|
3366
|
-
* Check whether a fully-covered service has operations whose overlay-mapped
|
|
3367
|
-
* methods are missing from the baseline class.
|
|
3368
|
-
*/
|
|
3369
|
-
function hasMethodsAbsentFromBaseline(service, ctx) {
|
|
3370
|
-
const baselineClasses = ctx.apiSurface?.classes;
|
|
3371
|
-
if (!baselineClasses) return false;
|
|
3372
|
-
const mountTarget = getMountTarget(service, ctx);
|
|
3373
|
-
if (mountTarget !== toPascalCase(service.name)) {
|
|
3374
|
-
const cls = baselineClasses[mountTarget];
|
|
3375
|
-
if (!cls) return true;
|
|
3376
|
-
for (const op of service.operations) {
|
|
3377
|
-
const method = resolveMethodName$6(op, service, ctx);
|
|
3378
|
-
if (!cls.methods?.[method]) return true;
|
|
3379
|
-
}
|
|
3380
|
-
return false;
|
|
3381
|
-
}
|
|
3382
|
-
const overlay = ctx.overlayLookup?.methodByOperation;
|
|
3383
|
-
if (!overlay) return false;
|
|
3384
|
-
for (const op of service.operations) {
|
|
3385
|
-
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
3386
|
-
const match = overlay.get(httpKey);
|
|
3387
|
-
if (!match) continue;
|
|
3388
|
-
const cls = baselineClasses[match.className];
|
|
3389
|
-
if (!cls) continue;
|
|
3390
|
-
if (!cls.methods?.[match.methodName]) return true;
|
|
3391
|
-
}
|
|
3392
|
-
return false;
|
|
3393
|
-
}
|
|
3394
|
-
/**
|
|
3395
|
-
* Check whether an IR model has fields not present in the baseline interface.
|
|
3396
|
-
*
|
|
3397
|
-
* When the live SDK exposes the same name as a type alias (e.g.
|
|
3398
|
-
* `type Role = EnvironmentRole | OrganizationRole;`), treat it as already
|
|
3399
|
-
* fully covered — generating an interface against an existing alias would
|
|
3400
|
-
* collide. The alias's referenced types still get generated independently
|
|
3401
|
-
* and serve as the canonical implementation.
|
|
3402
|
-
*/
|
|
3403
|
-
function modelHasNewFields(model, ctx) {
|
|
3404
|
-
if (!ctx.apiSurface?.interfaces && !ctx.apiSurface?.typeAliases) return true;
|
|
3405
|
-
const domainName = resolveInterfaceName(model.name, ctx);
|
|
3406
|
-
if (ctx.apiSurface?.typeAliases?.[domainName]) return false;
|
|
3407
|
-
const baseline = ctx.apiSurface?.interfaces?.[domainName];
|
|
3408
|
-
if (!baseline?.fields) return true;
|
|
3409
|
-
for (const field of model.fields) {
|
|
3410
|
-
const camelName = fieldName$6(field.name);
|
|
3411
|
-
if (!baseline.fields[camelName]) return true;
|
|
3412
|
-
}
|
|
3413
|
-
return false;
|
|
3414
|
-
}
|
|
3415
|
-
/**
|
|
3416
|
-
* Return operations in a service that are NOT covered by existing hand-written
|
|
3417
|
-
* service classes.
|
|
3418
|
-
*/
|
|
3419
|
-
function uncoveredOperations(service, ctx) {
|
|
3420
|
-
const overlay = ctx.overlayLookup?.methodByOperation;
|
|
3421
|
-
if (!overlay || overlay.size === 0) return service.operations;
|
|
3422
|
-
const baselineClasses = ctx.apiSurface?.classes;
|
|
3423
|
-
if (!baselineClasses) return service.operations;
|
|
3424
|
-
const existingClassNames = new Set(Object.keys(baselineClasses));
|
|
3425
|
-
return service.operations.filter((op) => {
|
|
3426
|
-
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
3427
|
-
const match = overlay.get(httpKey);
|
|
3428
|
-
if (!match) return true;
|
|
3429
|
-
return !existingClassNames.has(match.className);
|
|
3430
|
-
});
|
|
3431
|
-
}
|
|
3432
|
-
/**
|
|
3433
|
-
* Compute the set of model names reachable from non-event service operations.
|
|
3434
|
-
*/
|
|
3435
|
-
function computeNonEventReachable(services, models) {
|
|
3436
|
-
const seeds = /* @__PURE__ */ new Set();
|
|
3437
|
-
for (const svc of services) {
|
|
3438
|
-
if (svc.name.toLowerCase() === "events") continue;
|
|
3439
|
-
for (const op of svc.operations) {
|
|
3440
|
-
const collectFromRef = (t) => {
|
|
3441
|
-
if (!t) return;
|
|
3442
|
-
if (t.kind === "model") seeds.add(t.name);
|
|
3443
|
-
if (t.kind === "array") collectFromRef(t.items);
|
|
3444
|
-
if (t.kind === "nullable") collectFromRef(t.inner);
|
|
3445
|
-
if (t.kind === "union") t.variants.forEach(collectFromRef);
|
|
3446
|
-
};
|
|
3447
|
-
collectFromRef(op.response);
|
|
3448
|
-
collectFromRef(op.requestBody);
|
|
3449
|
-
if (op.pagination?.itemType) collectFromRef(op.pagination.itemType);
|
|
3450
|
-
}
|
|
3451
|
-
}
|
|
3452
|
-
const modelMap = new Map(models.map((m) => [m.name, m]));
|
|
3453
|
-
const reachable = /* @__PURE__ */ new Set();
|
|
3454
|
-
const queue = [...seeds];
|
|
3455
|
-
while (queue.length > 0) {
|
|
3456
|
-
const name = queue.pop();
|
|
3457
|
-
if (reachable.has(name)) continue;
|
|
3458
|
-
reachable.add(name);
|
|
3459
|
-
const m = modelMap.get(name);
|
|
3460
|
-
if (!m) continue;
|
|
3461
|
-
for (const field of m.fields) {
|
|
3462
|
-
const walk = (t) => {
|
|
3463
|
-
if (t.kind === "model" && !reachable.has(t.name)) queue.push(t.name);
|
|
3464
|
-
if (t.kind === "array") walk(t.items);
|
|
3465
|
-
if (t.kind === "nullable") walk(t.inner);
|
|
3466
|
-
if (t.kind === "union") t.variants.forEach(walk);
|
|
3467
|
-
};
|
|
3468
|
-
walk(field.type);
|
|
3469
|
-
}
|
|
3470
|
-
}
|
|
3471
|
-
return reachable;
|
|
3472
|
-
}
|
|
3473
|
-
//#endregion
|
|
3474
|
-
//#region src/node/live-surface.ts
|
|
3475
|
-
const SRC_DIR = "src";
|
|
3476
|
-
/**
|
|
3477
|
-
* Walk `${rootDir}/src/` and build a live-surface snapshot.
|
|
3478
|
-
*
|
|
3479
|
-
* If `${rootDir}/src/` does not exist, returns an empty surface (greenfield).
|
|
3480
|
-
* Reads each `.ts` file once; ignores `.spec.ts` and `.test.ts`.
|
|
3481
|
-
*/
|
|
3482
|
-
function buildLiveSurface(rootDir) {
|
|
3483
|
-
const surface = {
|
|
3484
|
-
rootDir,
|
|
3485
|
-
files: /* @__PURE__ */ new Set(),
|
|
3486
|
-
trackedFiles: loadGitTrackedFiles(rootDir),
|
|
3487
|
-
protectedFiles: /* @__PURE__ */ new Set(),
|
|
3488
|
-
autogenFiles: /* @__PURE__ */ new Set(),
|
|
3489
|
-
classes: /* @__PURE__ */ new Map(),
|
|
3490
|
-
interfaces: /* @__PURE__ */ new Map(),
|
|
3491
|
-
functions: /* @__PURE__ */ new Map(),
|
|
3492
|
-
constObjectEnums: /* @__PURE__ */ new Map()
|
|
3493
|
-
};
|
|
3494
|
-
const srcRoot = path$1.join(rootDir, SRC_DIR);
|
|
3495
|
-
if (!fs$1.existsSync(srcRoot) || !fs$1.statSync(srcRoot).isDirectory()) return surface;
|
|
3496
|
-
for (const absPath of walk$1(srcRoot)) {
|
|
3497
|
-
const rel = toPosix(path$1.relative(rootDir, absPath));
|
|
3498
|
-
surface.files.add(rel);
|
|
3499
|
-
if (!rel.endsWith(".ts")) continue;
|
|
3500
|
-
if (surface.trackedFiles.size > 0 && !surface.trackedFiles.has(rel)) continue;
|
|
3501
|
-
let text;
|
|
3502
|
-
try {
|
|
3503
|
-
text = fs$1.readFileSync(absPath, "utf8");
|
|
3504
|
-
} catch {
|
|
3505
|
-
continue;
|
|
3506
|
-
}
|
|
3507
|
-
if (isProtected(text)) surface.protectedFiles.add(rel);
|
|
3508
|
-
else if (isAutogen(text)) surface.autogenFiles.add(rel);
|
|
3509
|
-
if (rel.endsWith(".spec.ts") || rel.endsWith(".test.ts")) continue;
|
|
3510
|
-
extractDeclarations(text, rel, surface);
|
|
3511
|
-
}
|
|
3512
|
-
return surface;
|
|
3513
|
-
}
|
|
3514
|
-
/**
|
|
3515
|
-
* Module-level snapshot of the active live-surface, set by `index.ts` once per
|
|
3516
|
-
* generation run. Used by callers that don't have a clean way to thread
|
|
3517
|
-
* `EmitterContext` through (e.g. deeply-nested helpers in `resources.ts`
|
|
3518
|
-
* that decide whether to inline a serializer call).
|
|
3519
|
-
*/
|
|
3520
|
-
let activeSurface = null;
|
|
3521
|
-
function setActiveLiveSurface(surface) {
|
|
3522
|
-
activeSurface = surface;
|
|
3523
|
-
}
|
|
3524
|
-
function liveSurfaceHasFunction(name) {
|
|
3525
|
-
return activeSurface?.functions.has(name) ?? false;
|
|
3526
|
-
}
|
|
3527
|
-
/** Returns the relative file path containing the function, if known. */
|
|
3528
|
-
function liveSurfaceFunctionPath(name) {
|
|
3529
|
-
return activeSurface?.functions.get(name);
|
|
3530
|
-
}
|
|
3531
|
-
function liveSurfaceHasFile(relPath) {
|
|
3532
|
-
return activeSurface?.files.has(relPath) ?? false;
|
|
3351
|
+
function liveSurfaceHasFile(relPath) {
|
|
3352
|
+
return activeSurface?.files.has(relPath) ?? false;
|
|
3533
3353
|
}
|
|
3534
3354
|
function liveSurfaceHasManagedFile(relPath) {
|
|
3535
3355
|
const surface = activeSurface;
|
|
@@ -3659,68 +3479,741 @@ function extractDeclarations(text, relPath, surface) {
|
|
|
3659
3479
|
const value = memberMatch[4];
|
|
3660
3480
|
if (memberName && value !== void 0) members.set(value, memberName);
|
|
3661
3481
|
}
|
|
3662
|
-
if (members.size > 0) surface.constObjectEnums.set(enumName, members);
|
|
3482
|
+
if (members.size > 0) surface.constObjectEnums.set(enumName, members);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
const CONST_OBJECT_ENUM_RE = /^\s*export\s+const\s+([A-Z][\w$]*)\s*=\s*\{([\s\S]*?)\}\s*as\s+const\s*;/gm;
|
|
3486
|
+
const CONST_OBJECT_MEMBER_RE = /(?:'([^']+)'|"([^"]+)"|([a-zA-Z_$][\w$]*))\s*:\s*'([^']*)'/g;
|
|
3487
|
+
const METHOD_KEYWORD_BLOCKLIST = new Set([
|
|
3488
|
+
"if",
|
|
3489
|
+
"for",
|
|
3490
|
+
"while",
|
|
3491
|
+
"switch",
|
|
3492
|
+
"catch",
|
|
3493
|
+
"return",
|
|
3494
|
+
"throw",
|
|
3495
|
+
"constructor"
|
|
3496
|
+
]);
|
|
3497
|
+
const CLASS_MEMBER_RE = /^ {2}(?:(?:public|private|protected|readonly|static)\s+)*(?:async\s+)?([a-zA-Z_$][\w$]*)\s*(?:<[^>]*>)?\s*\(/;
|
|
3498
|
+
/**
|
|
3499
|
+
* Advance a brace-balance scan across one `line`, mutating `s`. Counts only
|
|
3500
|
+
* braces that live in *code* — including template `${ … }` interpolation,
|
|
3501
|
+
* whose braces are self-balancing — and ignores braces inside string/template
|
|
3502
|
+
* text, line comments, and block comments. State threads across lines so block
|
|
3503
|
+
* comments and multi-line template literals are tracked correctly.
|
|
3504
|
+
*
|
|
3505
|
+
* Naive character counting (the prior approach) would clip a method block at a
|
|
3506
|
+
* `}` that merely sat inside a comment (`// returns when done }`) or a string
|
|
3507
|
+
* (`'closing brace: }'`), appending a truncated, unbalanced body. Regex
|
|
3508
|
+
* literals are intentionally not modelled: distinguishing `/` division from a
|
|
3509
|
+
* regex needs full tokenization, and a regex whose braces are net-unbalanced
|
|
3510
|
+
* does not occur in generated or hand-edited SDK code (`{n,m}` quantifiers are
|
|
3511
|
+
* always balanced).
|
|
3512
|
+
*/
|
|
3513
|
+
function advanceBraceScan(line, s) {
|
|
3514
|
+
for (let i = 0; i < line.length; i++) {
|
|
3515
|
+
const c = line[i];
|
|
3516
|
+
if (s.inBlockComment) {
|
|
3517
|
+
if (c === "*" && line[i + 1] === "/") {
|
|
3518
|
+
s.inBlockComment = false;
|
|
3519
|
+
i++;
|
|
3520
|
+
}
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
const tdepth = s.templates.length;
|
|
3524
|
+
if (tdepth > 0 && s.templates[tdepth - 1] === 0) {
|
|
3525
|
+
if (c === "\\") i++;
|
|
3526
|
+
else if (c === "`") s.templates.pop();
|
|
3527
|
+
else if (c === "$" && line[i + 1] === "{") {
|
|
3528
|
+
s.templates[tdepth - 1] = 1;
|
|
3529
|
+
s.depth++;
|
|
3530
|
+
s.opened = true;
|
|
3531
|
+
i++;
|
|
3532
|
+
}
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
if (c === "/" && line[i + 1] === "/") break;
|
|
3536
|
+
if (c === "/" && line[i + 1] === "*") {
|
|
3537
|
+
s.inBlockComment = true;
|
|
3538
|
+
i++;
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
if (c === "'" || c === "\"") {
|
|
3542
|
+
const quote = c;
|
|
3543
|
+
i++;
|
|
3544
|
+
while (i < line.length && line[i] !== quote) {
|
|
3545
|
+
if (line[i] === "\\") i++;
|
|
3546
|
+
i++;
|
|
3547
|
+
}
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
if (c === "`") {
|
|
3551
|
+
s.templates.push(0);
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
if (c === "{") {
|
|
3555
|
+
s.depth++;
|
|
3556
|
+
s.opened = true;
|
|
3557
|
+
if (tdepth > 0) s.templates[tdepth - 1]++;
|
|
3558
|
+
} else if (c === "}") {
|
|
3559
|
+
s.depth--;
|
|
3560
|
+
if (tdepth > 0) s.templates[tdepth - 1]--;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
function parseClassesByLine(lines) {
|
|
3565
|
+
const classes = [];
|
|
3566
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3567
|
+
const decl = lines[i].match(/^export\s+(?:abstract\s+)?class\s+([A-Z][\w$]*)/);
|
|
3568
|
+
if (!decl) continue;
|
|
3569
|
+
const scan = {
|
|
3570
|
+
depth: 0,
|
|
3571
|
+
opened: false,
|
|
3572
|
+
inBlockComment: false,
|
|
3573
|
+
templates: []
|
|
3574
|
+
};
|
|
3575
|
+
let closeLine = -1;
|
|
3576
|
+
for (let j = i; j < lines.length; j++) {
|
|
3577
|
+
advanceBraceScan(lines[j], scan);
|
|
3578
|
+
if (scan.opened && scan.depth <= 0) {
|
|
3579
|
+
closeLine = j;
|
|
3580
|
+
break;
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
if (closeLine < 0) continue;
|
|
3584
|
+
const methods = [];
|
|
3585
|
+
for (let j = i + 1; j < closeLine; j++) {
|
|
3586
|
+
const member = lines[j].match(CLASS_MEMBER_RE);
|
|
3587
|
+
if (!member) continue;
|
|
3588
|
+
const name = member[1];
|
|
3589
|
+
if (name === "constructor" || METHOD_KEYWORD_BLOCKLIST.has(name)) continue;
|
|
3590
|
+
if (!methods.includes(name)) methods.push(name);
|
|
3591
|
+
}
|
|
3592
|
+
classes.push({
|
|
3593
|
+
name: decl[1],
|
|
3594
|
+
declLine: i,
|
|
3595
|
+
closeLine,
|
|
3596
|
+
methods
|
|
3597
|
+
});
|
|
3598
|
+
i = closeLine;
|
|
3599
|
+
}
|
|
3600
|
+
return classes;
|
|
3601
|
+
}
|
|
3602
|
+
/**
|
|
3603
|
+
* Extract the full source block of `method` (attached comments included) from
|
|
3604
|
+
* a parsed class. Returns null if the block cannot be delimited.
|
|
3605
|
+
*/
|
|
3606
|
+
function extractMethodBlock(lines, cls, method) {
|
|
3607
|
+
for (let j = cls.declLine + 1; j < cls.closeLine; j++) {
|
|
3608
|
+
const member = lines[j].match(CLASS_MEMBER_RE);
|
|
3609
|
+
if (!member || member[1] !== method) continue;
|
|
3610
|
+
let start = j;
|
|
3611
|
+
while (start > cls.declLine + 1) {
|
|
3612
|
+
const above = lines[start - 1].trim();
|
|
3613
|
+
if (above.startsWith("//") || above.startsWith("/*") || above.startsWith("*")) start--;
|
|
3614
|
+
else break;
|
|
3615
|
+
}
|
|
3616
|
+
const scan = {
|
|
3617
|
+
depth: 0,
|
|
3618
|
+
opened: false,
|
|
3619
|
+
inBlockComment: false,
|
|
3620
|
+
templates: []
|
|
3621
|
+
};
|
|
3622
|
+
for (let end = j; end < cls.closeLine; end++) {
|
|
3623
|
+
advanceBraceScan(lines[end], scan);
|
|
3624
|
+
if (scan.opened && scan.depth <= 0) return lines.slice(start, end + 1);
|
|
3625
|
+
}
|
|
3626
|
+
return null;
|
|
3627
|
+
}
|
|
3628
|
+
return null;
|
|
3629
|
+
}
|
|
3630
|
+
const IMPORT_STMT_RE = /^import\s+(type\s+)?(?:([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([\s\S]*?)\}\s*)?(?:\*\s+as\s+([A-Za-z_$][\w$]*)\s+)?from\s+['"]([^'"]+)['"];?/gm;
|
|
3631
|
+
function importLocalName(entry) {
|
|
3632
|
+
const asMatch = entry.match(/\s+as\s+([A-Za-z_$][\w$]*)\s*$/);
|
|
3633
|
+
if (asMatch) return asMatch[1];
|
|
3634
|
+
return entry.replace(/^type\s+/, "").trim();
|
|
3635
|
+
}
|
|
3636
|
+
function collectImportedLocalNames(text) {
|
|
3637
|
+
const names = /* @__PURE__ */ new Set();
|
|
3638
|
+
for (const m of text.matchAll(IMPORT_STMT_RE)) {
|
|
3639
|
+
if (m[2]) names.add(m[2]);
|
|
3640
|
+
if (m[4]) names.add(m[4]);
|
|
3641
|
+
for (const entry of (m[3] ?? "").split(",")) {
|
|
3642
|
+
const trimmed = entry.trim();
|
|
3643
|
+
if (trimmed) names.add(importLocalName(trimmed));
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
return names;
|
|
3647
|
+
}
|
|
3648
|
+
/** Import statements from `generatedText` whose names `existingText` lacks. */
|
|
3649
|
+
function missingImportStatements(existingText, generatedText) {
|
|
3650
|
+
const existingNames = collectImportedLocalNames(existingText);
|
|
3651
|
+
const statements = [];
|
|
3652
|
+
for (const m of generatedText.matchAll(IMPORT_STMT_RE)) {
|
|
3653
|
+
const [stmt, typeOnly, defaultName, named, namespaceName, moduleSpec] = m;
|
|
3654
|
+
if (defaultName || namespaceName) {
|
|
3655
|
+
const local = defaultName ?? namespaceName;
|
|
3656
|
+
if (local && !existingNames.has(local)) statements.push(stmt.endsWith(";") ? stmt : `${stmt};`);
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
const missing = (named ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry && !existingNames.has(importLocalName(entry)));
|
|
3660
|
+
if (missing.length === 0) continue;
|
|
3661
|
+
statements.push(`import ${typeOnly ? "type " : ""}{ ${missing.join(", ")} } from '${moduleSpec}';`);
|
|
3662
|
+
}
|
|
3663
|
+
return statements;
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Merge a partial class emission into the existing on-disk file content.
|
|
3667
|
+
*
|
|
3668
|
+
* Acts only when a class declared in BOTH texts has methods on disk that the
|
|
3669
|
+
* generated content drops — the signature of a "new operations only" partial
|
|
3670
|
+
* emission. Returns the existing text with the generated-only methods appended
|
|
3671
|
+
* to the class body (and missing imports added), preserving every existing
|
|
3672
|
+
* method and import verbatim.
|
|
3673
|
+
*
|
|
3674
|
+
* Returns null when the generated content does not drop any existing method;
|
|
3675
|
+
* callers should then proceed with their normal (overwrite) path so full
|
|
3676
|
+
* regenerations keep propagating emitter improvements and spec renames.
|
|
3677
|
+
*/
|
|
3678
|
+
function mergeGeneratedClassMethodsIntoExisting(existingText, generatedText) {
|
|
3679
|
+
const existingLines = existingText.split("\n");
|
|
3680
|
+
const generatedLines = generatedText.split("\n");
|
|
3681
|
+
const existingClasses = parseClassesByLine(existingLines);
|
|
3682
|
+
if (existingClasses.length === 0) return null;
|
|
3683
|
+
const generatedClasses = new Map(parseClassesByLine(generatedLines).map((cls) => [cls.name, cls]));
|
|
3684
|
+
let dropsExistingMethod = false;
|
|
3685
|
+
const insertions = [];
|
|
3686
|
+
for (const existing of existingClasses) {
|
|
3687
|
+
const generated = generatedClasses.get(existing.name);
|
|
3688
|
+
if (!generated) continue;
|
|
3689
|
+
const generatedMethods = new Set(generated.methods);
|
|
3690
|
+
if (!existing.methods.some((method) => !generatedMethods.has(method))) continue;
|
|
3691
|
+
dropsExistingMethod = true;
|
|
3692
|
+
const existingMethods = new Set(existing.methods);
|
|
3693
|
+
const blocks = [];
|
|
3694
|
+
for (const method of generated.methods) {
|
|
3695
|
+
if (existingMethods.has(method)) continue;
|
|
3696
|
+
const block = extractMethodBlock(generatedLines, generated, method);
|
|
3697
|
+
if (block) blocks.push(block);
|
|
3698
|
+
}
|
|
3699
|
+
if (blocks.length > 0) insertions.push({
|
|
3700
|
+
atLine: existing.closeLine,
|
|
3701
|
+
blocks
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
if (!dropsExistingMethod) return null;
|
|
3705
|
+
const merged = [...existingLines];
|
|
3706
|
+
insertions.sort((a, b) => b.atLine - a.atLine);
|
|
3707
|
+
for (const insertion of insertions) {
|
|
3708
|
+
const blockLines = [];
|
|
3709
|
+
for (const block of insertion.blocks) blockLines.push("", ...block);
|
|
3710
|
+
merged.splice(insertion.atLine, 0, ...blockLines);
|
|
3711
|
+
}
|
|
3712
|
+
const importStatements = missingImportStatements(existingText, generatedText);
|
|
3713
|
+
if (importStatements.length > 0) {
|
|
3714
|
+
let lastImportLine = -1;
|
|
3715
|
+
for (let i = 0; i < existingLines.length; i++) if (/from\s+['"][^'"]+['"];?\s*$/.test(existingLines[i]) || /^import\s+['"][^'"]+['"];?\s*$/.test(existingLines[i])) lastImportLine = i;
|
|
3716
|
+
if (lastImportLine >= 0) merged.splice(lastImportLine + 1, 0, ...importStatements);
|
|
3717
|
+
else {
|
|
3718
|
+
let insertAt = 0;
|
|
3719
|
+
while (insertAt < existingLines.length) {
|
|
3720
|
+
const line = existingLines[insertAt].trim();
|
|
3721
|
+
if (line === "" || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) insertAt++;
|
|
3722
|
+
else break;
|
|
3723
|
+
}
|
|
3724
|
+
merged.splice(insertAt, 0, ...importStatements, "");
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
return merged.join("\n");
|
|
3728
|
+
}
|
|
3729
|
+
/**
|
|
3730
|
+
* Given the start index of a class declaration, return the brace-delimited body
|
|
3731
|
+
* as a substring. Returns '' if the body cannot be located.
|
|
3732
|
+
*/
|
|
3733
|
+
function sliceClassBody(text, start) {
|
|
3734
|
+
const open = text.indexOf("{", start);
|
|
3735
|
+
if (open < 0) return "";
|
|
3736
|
+
let depth = 0;
|
|
3737
|
+
for (let i = open; i < text.length; i++) {
|
|
3738
|
+
const c = text[i];
|
|
3739
|
+
if (c === "{") depth++;
|
|
3740
|
+
else if (c === "}") {
|
|
3741
|
+
depth--;
|
|
3742
|
+
if (depth === 0) return text.slice(open + 1, i);
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
return "";
|
|
3746
|
+
}
|
|
3747
|
+
/**
|
|
3748
|
+
* Pull field names from an interface body. Conservative: only matches simple
|
|
3749
|
+
* declarations of the form `name?: ...;` or `name: ...;`. Generics, methods,
|
|
3750
|
+
* and computed property names are skipped (they don't influence skip decisions).
|
|
3751
|
+
*/
|
|
3752
|
+
function extractInterfaceFields(text, start) {
|
|
3753
|
+
const fields = /* @__PURE__ */ new Set();
|
|
3754
|
+
const open = text.indexOf("{", start);
|
|
3755
|
+
if (open < 0) return fields;
|
|
3756
|
+
let depth = 0;
|
|
3757
|
+
let close = -1;
|
|
3758
|
+
for (let i = open; i < text.length; i++) {
|
|
3759
|
+
const c = text[i];
|
|
3760
|
+
if (c === "{") depth++;
|
|
3761
|
+
else if (c === "}") {
|
|
3762
|
+
depth--;
|
|
3763
|
+
if (depth === 0) {
|
|
3764
|
+
close = i;
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
if (close < 0) return fields;
|
|
3770
|
+
const body = text.slice(open + 1, close);
|
|
3771
|
+
for (const m of body.matchAll(/^\s*(?:readonly\s+)?(?:'([^']+)'|"([^"]+)"|([a-zA-Z_$][\w$]*))\s*\??\s*:/gm)) {
|
|
3772
|
+
const name = m[1] ?? m[2] ?? m[3];
|
|
3773
|
+
if (name) fields.add(name);
|
|
3774
|
+
}
|
|
3775
|
+
return fields;
|
|
3776
|
+
}
|
|
3777
|
+
//#endregion
|
|
3778
|
+
//#region src/node/utils.ts
|
|
3779
|
+
/**
|
|
3780
|
+
* Compute a relative import path between two files within the generated SDK.
|
|
3781
|
+
*/
|
|
3782
|
+
function relativeImport(fromFile, toFile) {
|
|
3783
|
+
const fromDir = fromFile.split("/").slice(0, -1);
|
|
3784
|
+
const toFileParts = toFile.split("/");
|
|
3785
|
+
const toDir = toFileParts.slice(0, -1);
|
|
3786
|
+
const toFileName = toFileParts[toFileParts.length - 1];
|
|
3787
|
+
let common = 0;
|
|
3788
|
+
while (common < fromDir.length && common < toDir.length && fromDir[common] === toDir[common]) common++;
|
|
3789
|
+
const ups = fromDir.length - common;
|
|
3790
|
+
const downs = toDir.slice(common);
|
|
3791
|
+
let result = [
|
|
3792
|
+
...Array(ups).fill(".."),
|
|
3793
|
+
...downs,
|
|
3794
|
+
toFileName
|
|
3795
|
+
].join("/");
|
|
3796
|
+
result = result.replace(/\.ts$/, "");
|
|
3797
|
+
if (!result.startsWith(".")) result = "./" + result;
|
|
3798
|
+
return result;
|
|
3799
|
+
}
|
|
3800
|
+
/**
|
|
3801
|
+
* Render a JSDoc comment block from a description string.
|
|
3802
|
+
*/
|
|
3803
|
+
function docComment$2(description, indent = 0) {
|
|
3804
|
+
const pad = " ".repeat(indent);
|
|
3805
|
+
const descLines = description.split("\n");
|
|
3806
|
+
if (descLines.length === 1) return [`${pad}/** ${descLines[0]} */`];
|
|
3807
|
+
const lines = [`${pad}/**`];
|
|
3808
|
+
for (const line of descLines) lines.push(line === "" ? `${pad} *` : `${pad} * ${line}`);
|
|
3809
|
+
lines.push(`${pad} */`);
|
|
3810
|
+
return lines;
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* Build a map from model name -> default type args string for generic models.
|
|
3814
|
+
*/
|
|
3815
|
+
function buildGenericModelDefaults(models) {
|
|
3816
|
+
const result = /* @__PURE__ */ new Map();
|
|
3817
|
+
for (const model of models) {
|
|
3818
|
+
if (!model.typeParams?.length) continue;
|
|
3819
|
+
const defaults = model.typeParams.map((tp) => tp.default ? mapTypeRef$7(tp.default) : "unknown");
|
|
3820
|
+
result.set(model.name, `<${defaults.join(", ")}>`);
|
|
3821
|
+
}
|
|
3822
|
+
return result;
|
|
3823
|
+
}
|
|
3824
|
+
/**
|
|
3825
|
+
* Remove unused imports from generated source code.
|
|
3826
|
+
*/
|
|
3827
|
+
function pruneUnusedImports(lines) {
|
|
3828
|
+
const importLines = [];
|
|
3829
|
+
const bodyLines = [];
|
|
3830
|
+
let inBody = false;
|
|
3831
|
+
for (const line of lines) if (!inBody && (line.startsWith("import ") || line === "")) importLines.push(line);
|
|
3832
|
+
else {
|
|
3833
|
+
inBody = true;
|
|
3834
|
+
bodyLines.push(line);
|
|
3835
|
+
}
|
|
3836
|
+
const body = bodyLines.join("\n");
|
|
3837
|
+
const kept = [];
|
|
3838
|
+
for (const line of importLines) {
|
|
3839
|
+
if (line === "") {
|
|
3840
|
+
kept.push(line);
|
|
3841
|
+
continue;
|
|
3842
|
+
}
|
|
3843
|
+
const match = line.match(/\{([^}]+)\}/);
|
|
3844
|
+
if (!match) {
|
|
3845
|
+
kept.push(line);
|
|
3846
|
+
continue;
|
|
3847
|
+
}
|
|
3848
|
+
const names = match[1].split(",").map((n) => n.trim()).filter(Boolean);
|
|
3849
|
+
const usedNames = names.filter((name) => {
|
|
3850
|
+
return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(body);
|
|
3851
|
+
});
|
|
3852
|
+
if (usedNames.length === 0) continue;
|
|
3853
|
+
if (usedNames.length === names.length) kept.push(line);
|
|
3854
|
+
else {
|
|
3855
|
+
const isTypeImport = line.startsWith("import type");
|
|
3856
|
+
const fromMatch = line.match(/from\s+['"]([^'"]+)['"]/);
|
|
3857
|
+
if (fromMatch) {
|
|
3858
|
+
const prefix = isTypeImport ? "import type" : "import";
|
|
3859
|
+
kept.push(`${prefix} { ${usedNames.join(", ")} } from '${fromMatch[1]}';`);
|
|
3860
|
+
} else kept.push(line);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
return [...kept, ...bodyLines];
|
|
3864
|
+
}
|
|
3865
|
+
/** Built-in TypeScript types that are always available. */
|
|
3866
|
+
const TS_BUILTINS = new Set([
|
|
3867
|
+
"Record",
|
|
3868
|
+
"Promise",
|
|
3869
|
+
"Array",
|
|
3870
|
+
"Map",
|
|
3871
|
+
"Set",
|
|
3872
|
+
"Date",
|
|
3873
|
+
"string",
|
|
3874
|
+
"number",
|
|
3875
|
+
"boolean",
|
|
3876
|
+
"void",
|
|
3877
|
+
"null",
|
|
3878
|
+
"undefined",
|
|
3879
|
+
"any",
|
|
3880
|
+
"never",
|
|
3881
|
+
"unknown",
|
|
3882
|
+
"true",
|
|
3883
|
+
"false"
|
|
3884
|
+
]);
|
|
3885
|
+
/**
|
|
3886
|
+
* Build a comprehensive set of all known type names from the IR and baseline.
|
|
3887
|
+
*/
|
|
3888
|
+
function buildKnownTypeNames(models, ctx) {
|
|
3889
|
+
const knownNames = /* @__PURE__ */ new Set();
|
|
3890
|
+
for (const m of models) knownNames.add(resolveInterfaceName(m.name, ctx));
|
|
3891
|
+
for (const e of ctx.spec.enums) knownNames.add(e.name);
|
|
3892
|
+
if (ctx.apiSurface?.interfaces) for (const name of Object.keys(ctx.apiSurface.interfaces)) knownNames.add(name);
|
|
3893
|
+
if (ctx.apiSurface?.typeAliases) for (const name of Object.keys(ctx.apiSurface.typeAliases)) knownNames.add(name);
|
|
3894
|
+
if (ctx.apiSurface?.enums) for (const name of Object.keys(ctx.apiSurface.enums)) knownNames.add(name);
|
|
3895
|
+
return knownNames;
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Create a service directory resolver bundle.
|
|
3899
|
+
*
|
|
3900
|
+
* When `ctx.apiSurface` is populated, the baseline `sourceFile` of an
|
|
3901
|
+
* existing interface wins over the IR-derived first-reference assignment.
|
|
3902
|
+
* This keeps generated imports pointing at the existing live SDK location
|
|
3903
|
+
* instead of duplicating a model into a different service directory.
|
|
3904
|
+
*/
|
|
3905
|
+
function createServiceDirResolver(models, services, ctx) {
|
|
3906
|
+
const modelToService = assignModelsToEmittableServices(models, services, ctx);
|
|
3907
|
+
const serviceNameMap = buildServiceNameMap(services, ctx);
|
|
3908
|
+
const baselineDirByModel = harvestBaselineDirByModel(ctx);
|
|
3909
|
+
for (const [modelName] of modelToService) {
|
|
3910
|
+
const dir = baselineDirByModel.get(modelName);
|
|
3911
|
+
if (!dir) continue;
|
|
3912
|
+
const synthetic = `__baseline_dir__:${dir}`;
|
|
3913
|
+
modelToService.set(modelName, synthetic);
|
|
3914
|
+
if (!serviceNameMap.has(synthetic)) serviceNameMap.set(synthetic, dir);
|
|
3915
|
+
}
|
|
3916
|
+
const resolveDir = (irService) => {
|
|
3917
|
+
if (!irService) return "common";
|
|
3918
|
+
if (irService.startsWith("__baseline_dir__:")) return irService.slice(17);
|
|
3919
|
+
return resolveServiceDir$1(serviceNameMap.get(irService) ?? irService);
|
|
3920
|
+
};
|
|
3921
|
+
return {
|
|
3922
|
+
modelToService,
|
|
3923
|
+
serviceNameMap,
|
|
3924
|
+
resolveDir
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
/**
|
|
3928
|
+
* Map baseline interface / type-alias names to the top-level `src/<dir>/`
|
|
3929
|
+
* their `sourceFile` lives in. Both kinds can shadow IR model names — e.g.
|
|
3930
|
+
* `type Role = EnvironmentRole | OrganizationRole;` is the live SDK's
|
|
3931
|
+
* canonical Role definition even though the IR represents Role as a model.
|
|
3932
|
+
*/
|
|
3933
|
+
function harvestBaselineDirByModel(ctx) {
|
|
3934
|
+
const baselineDirByModel = /* @__PURE__ */ new Map();
|
|
3935
|
+
const recordSource = (name, info) => {
|
|
3936
|
+
const sourceFile = info?.sourceFile;
|
|
3937
|
+
if (!sourceFile) return;
|
|
3938
|
+
const m = sourceFile.match(/^src\/([^/]+)\//);
|
|
3939
|
+
if (!m) return;
|
|
3940
|
+
baselineDirByModel.set(name, m[1]);
|
|
3941
|
+
};
|
|
3942
|
+
for (const [name, info] of Object.entries(ctx.apiSurface?.interfaces ?? {})) recordSource(name, info);
|
|
3943
|
+
for (const [name, info] of Object.entries(ctx.apiSurface?.typeAliases ?? {})) if (!baselineDirByModel.has(name)) recordSource(name, info);
|
|
3944
|
+
return baselineDirByModel;
|
|
3945
|
+
}
|
|
3946
|
+
/**
|
|
3947
|
+
* `assignModelsToServices` plus an owned-service correction pass.
|
|
3948
|
+
*
|
|
3949
|
+
* The engine's assignment is first-reference-wins: a model referenced by both
|
|
3950
|
+
* Organizations and AuditLogs lands in `organizations/` even when only
|
|
3951
|
+
* AuditLogs is owned this run. Against an existing SDK, `applyLiveSurface`
|
|
3952
|
+
* then drops the model file (a non-owned, non-adopted directory cannot
|
|
3953
|
+
* receive new paths) while the owned resource still imports
|
|
3954
|
+
* `../organizations/interfaces/<model>.interface` — an import that resolves
|
|
3955
|
+
* to nothing (TS2307).
|
|
3956
|
+
*
|
|
3957
|
+
* The correction re-homes such models to the owned service that references
|
|
3958
|
+
* them, so emission and import planning agree on a directory that is allowed
|
|
3959
|
+
* to receive files. It only fires when:
|
|
3960
|
+
* 1. `ownedServices` is configured and the run targets an existing SDK;
|
|
3961
|
+
* 2. the model has no baseline `sourceFile` (otherwise the baseline-dir
|
|
3962
|
+
* override keeps imports pointing at the on-disk location);
|
|
3963
|
+
* 3. the model's computed interface path does not already exist on disk;
|
|
3964
|
+
* 4. the assigned service is neither owned itself nor sharing a directory
|
|
3965
|
+
* with an owned service.
|
|
3966
|
+
*/
|
|
3967
|
+
function assignModelsToEmittableServices(models, services, ctx) {
|
|
3968
|
+
const modelToService = assignModelsToServices(models, services, ctx?.modelHints);
|
|
3969
|
+
if (ctx) reassignOwnedServiceDependencies(modelToService, models, services, ctx);
|
|
3970
|
+
return modelToService;
|
|
3971
|
+
}
|
|
3972
|
+
function reassignOwnedServiceDependencies(modelToService, models, services, ctx) {
|
|
3973
|
+
const serviceNameMap = buildServiceNameMap(services, ctx);
|
|
3974
|
+
const mountGroups = groupByMount(ctx);
|
|
3975
|
+
const ownedServices = (mountGroups.size > 0 ? [...mountGroups].map(([name, group]) => ({
|
|
3976
|
+
name,
|
|
3977
|
+
operations: group.operations
|
|
3978
|
+
})) : services).filter((s) => isNodeOwnedService(ctx, s.name, serviceNameMap.get(s.name)));
|
|
3979
|
+
if (ownedServices.length === 0) return;
|
|
3980
|
+
if (!liveSurfaceHasExistingSdk()) return;
|
|
3981
|
+
const dirOf = (irService) => irService ? resolveServiceDir$1(serviceNameMap.get(irService) ?? irService) : "common";
|
|
3982
|
+
const ownedDirs = new Set(ownedServices.map((s) => dirOf(s.name)));
|
|
3983
|
+
const baselineDirByModel = harvestBaselineDirByModel(ctx);
|
|
3984
|
+
const modelsByName = new Map(models.map((m) => [m.name, m]));
|
|
3985
|
+
for (const service of ownedServices) for (const name of collectServiceModelClosure(service, modelsByName)) {
|
|
3986
|
+
if (!modelsByName.has(name)) continue;
|
|
3987
|
+
if (baselineDirByModel.has(name)) continue;
|
|
3988
|
+
const assigned = modelToService.get(name);
|
|
3989
|
+
if (assigned && isNodeOwnedService(ctx, assigned, serviceNameMap.get(assigned))) continue;
|
|
3990
|
+
const assignedDir = dirOf(assigned);
|
|
3991
|
+
if (ownedDirs.has(assignedDir)) continue;
|
|
3992
|
+
if (liveSurfaceHasFile(`src/${assignedDir}/interfaces/${fileName$3(name)}.interface.ts`)) continue;
|
|
3993
|
+
modelToService.set(name, service.name);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
/** Model names referenced by a service's operations, expanded through fields. */
|
|
3997
|
+
function collectServiceModelClosure(service, modelsByName) {
|
|
3998
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
3999
|
+
const add = (ref) => {
|
|
4000
|
+
if (!ref) return;
|
|
4001
|
+
for (const name of collectModelRefs(ref)) referenced.add(name);
|
|
4002
|
+
};
|
|
4003
|
+
for (const op of service.operations) {
|
|
4004
|
+
add(op.requestBody);
|
|
4005
|
+
add(op.response);
|
|
4006
|
+
for (const param of [
|
|
4007
|
+
...op.pathParams,
|
|
4008
|
+
...op.queryParams,
|
|
4009
|
+
...op.headerParams,
|
|
4010
|
+
...op.cookieParams ?? []
|
|
4011
|
+
]) add(param.type);
|
|
4012
|
+
if (op.pagination) add(op.pagination.itemType);
|
|
4013
|
+
}
|
|
4014
|
+
const queue = [...referenced];
|
|
4015
|
+
while (queue.length > 0) {
|
|
4016
|
+
const name = queue.pop();
|
|
4017
|
+
const model = modelsByName.get(name);
|
|
4018
|
+
if (!model) continue;
|
|
4019
|
+
for (const dep of collectFieldDependencies(model).models) if (!referenced.has(dep)) {
|
|
4020
|
+
referenced.add(dep);
|
|
4021
|
+
queue.push(dep);
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
return referenced;
|
|
4025
|
+
}
|
|
4026
|
+
/**
|
|
4027
|
+
* Check if baseline interface fields appear to contain generic type parameters.
|
|
4028
|
+
*
|
|
4029
|
+
* Heuristic: strip string literals first (so `'GoogleSAML'` is not mistaken
|
|
4030
|
+
* for a type name), then look for any PascalCase token that isn't a known
|
|
4031
|
+
* type — those indicate an unbound generic parameter like `TCustomAttributes`.
|
|
4032
|
+
*/
|
|
4033
|
+
function isBaselineGeneric(fields, knownNames) {
|
|
4034
|
+
for (const [, bf] of Object.entries(fields)) {
|
|
4035
|
+
const typeNames = bf.type.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "").match(/\b[A-Z][a-zA-Z0-9]*\b/g);
|
|
4036
|
+
if (!typeNames) continue;
|
|
4037
|
+
for (const tn of typeNames) {
|
|
4038
|
+
if (TS_BUILTINS.has(tn)) continue;
|
|
4039
|
+
if (knownNames.has(tn)) continue;
|
|
4040
|
+
return true;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
return false;
|
|
4044
|
+
}
|
|
4045
|
+
function modelFingerprint(model) {
|
|
4046
|
+
return model.fields.map((f) => `${f.name}:${JSON.stringify(f.type)}:${f.required}`).sort().join("|");
|
|
4047
|
+
}
|
|
4048
|
+
/**
|
|
4049
|
+
* Find structurally identical models and build a deduplication map.
|
|
4050
|
+
*/
|
|
4051
|
+
function buildDeduplicationMap(models, ctx, reachable) {
|
|
4052
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
4053
|
+
const fingerprints = /* @__PURE__ */ new Map();
|
|
4054
|
+
for (const model of models) {
|
|
4055
|
+
if (model.fields.length === 0) continue;
|
|
4056
|
+
const fp = modelFingerprint(model);
|
|
4057
|
+
const existing = fingerprints.get(fp);
|
|
4058
|
+
if (existing) if (reachable && !reachable.has(existing) && reachable.has(model.name)) {
|
|
4059
|
+
dedup.delete(existing);
|
|
4060
|
+
dedup.set(existing, model.name);
|
|
4061
|
+
fingerprints.set(fp, model.name);
|
|
4062
|
+
} else dedup.set(model.name, existing);
|
|
4063
|
+
else fingerprints.set(fp, model.name);
|
|
4064
|
+
}
|
|
4065
|
+
if (ctx) {
|
|
4066
|
+
const byDomainName = /* @__PURE__ */ new Map();
|
|
4067
|
+
for (const model of models) {
|
|
4068
|
+
if (model.fields.length === 0) continue;
|
|
4069
|
+
if (dedup.has(model.name)) continue;
|
|
4070
|
+
const domainName = resolveInterfaceName(model.name, ctx);
|
|
4071
|
+
const group = byDomainName.get(domainName);
|
|
4072
|
+
if (group) group.push(model);
|
|
4073
|
+
else byDomainName.set(domainName, [model]);
|
|
4074
|
+
}
|
|
4075
|
+
for (const [, group] of byDomainName) {
|
|
4076
|
+
if (group.length < 2) continue;
|
|
4077
|
+
group.sort((a, b) => {
|
|
4078
|
+
if (reachable) {
|
|
4079
|
+
const aReach = reachable.has(a.name) ? 0 : 1;
|
|
4080
|
+
const bReach = reachable.has(b.name) ? 0 : 1;
|
|
4081
|
+
if (aReach !== bReach) return aReach - bReach;
|
|
4082
|
+
}
|
|
4083
|
+
return b.fields.length - a.fields.length || a.name.localeCompare(b.name);
|
|
4084
|
+
});
|
|
4085
|
+
const canonical = group[0];
|
|
4086
|
+
for (let i = 1; i < group.length; i++) dedup.set(group[i].name, canonical.name);
|
|
4087
|
+
}
|
|
3663
4088
|
}
|
|
4089
|
+
return dedup;
|
|
3664
4090
|
}
|
|
3665
|
-
const CONST_OBJECT_ENUM_RE = /^\s*export\s+const\s+([A-Z][\w$]*)\s*=\s*\{([\s\S]*?)\}\s*as\s+const\s*;/gm;
|
|
3666
|
-
const CONST_OBJECT_MEMBER_RE = /(?:'([^']+)'|"([^"]+)"|([a-zA-Z_$][\w$]*))\s*:\s*'([^']*)'/g;
|
|
3667
|
-
const METHOD_KEYWORD_BLOCKLIST = new Set([
|
|
3668
|
-
"if",
|
|
3669
|
-
"for",
|
|
3670
|
-
"while",
|
|
3671
|
-
"switch",
|
|
3672
|
-
"catch",
|
|
3673
|
-
"return",
|
|
3674
|
-
"throw",
|
|
3675
|
-
"constructor"
|
|
3676
|
-
]);
|
|
3677
4091
|
/**
|
|
3678
|
-
*
|
|
3679
|
-
*
|
|
4092
|
+
* Check whether a service's endpoints are already fully covered by existing
|
|
4093
|
+
* hand-written service classes.
|
|
3680
4094
|
*/
|
|
3681
|
-
function
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
4095
|
+
function isServiceCoveredByExisting(service, ctx) {
|
|
4096
|
+
if (getMountTarget(service, ctx) !== toPascalCase(service.name)) return true;
|
|
4097
|
+
const overlay = ctx.overlayLookup?.methodByOperation;
|
|
4098
|
+
if (!overlay || overlay.size === 0) return false;
|
|
4099
|
+
if (service.operations.length === 0) return false;
|
|
4100
|
+
const baselineClasses = ctx.apiSurface?.classes;
|
|
4101
|
+
if (!baselineClasses) return false;
|
|
4102
|
+
const existingClassNames = new Set(Object.keys(baselineClasses));
|
|
4103
|
+
return service.operations.every((op) => {
|
|
4104
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
4105
|
+
const match = overlay.get(httpKey);
|
|
4106
|
+
if (!match) return false;
|
|
4107
|
+
return existingClassNames.has(match.className);
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
4110
|
+
/**
|
|
4111
|
+
* Check whether a fully-covered service has operations whose overlay-mapped
|
|
4112
|
+
* methods are missing from the baseline class.
|
|
4113
|
+
*/
|
|
4114
|
+
function hasMethodsAbsentFromBaseline(service, ctx) {
|
|
4115
|
+
const baselineClasses = ctx.apiSurface?.classes;
|
|
4116
|
+
if (!baselineClasses) return false;
|
|
4117
|
+
const mountTarget = getMountTarget(service, ctx);
|
|
4118
|
+
if (mountTarget !== toPascalCase(service.name)) {
|
|
4119
|
+
const cls = baselineClasses[mountTarget];
|
|
4120
|
+
if (!cls) return true;
|
|
4121
|
+
for (const op of service.operations) {
|
|
4122
|
+
const method = resolveMethodName$6(op, service, ctx);
|
|
4123
|
+
if (!cls.methods?.[method]) return true;
|
|
3691
4124
|
}
|
|
4125
|
+
return false;
|
|
3692
4126
|
}
|
|
3693
|
-
|
|
4127
|
+
const overlay = ctx.overlayLookup?.methodByOperation;
|
|
4128
|
+
if (!overlay) return false;
|
|
4129
|
+
for (const op of service.operations) {
|
|
4130
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
4131
|
+
const match = overlay.get(httpKey);
|
|
4132
|
+
if (!match) continue;
|
|
4133
|
+
const cls = baselineClasses[match.className];
|
|
4134
|
+
if (!cls) continue;
|
|
4135
|
+
if (!cls.methods?.[match.methodName]) return true;
|
|
4136
|
+
}
|
|
4137
|
+
return false;
|
|
3694
4138
|
}
|
|
3695
4139
|
/**
|
|
3696
|
-
*
|
|
3697
|
-
*
|
|
3698
|
-
*
|
|
4140
|
+
* Check whether an IR model has fields not present in the baseline interface.
|
|
4141
|
+
*
|
|
4142
|
+
* When the live SDK exposes the same name as a type alias (e.g.
|
|
4143
|
+
* `type Role = EnvironmentRole | OrganizationRole;`), treat it as already
|
|
4144
|
+
* fully covered — generating an interface against an existing alias would
|
|
4145
|
+
* collide. The alias's referenced types still get generated independently
|
|
4146
|
+
* and serve as the canonical implementation.
|
|
3699
4147
|
*/
|
|
3700
|
-
function
|
|
3701
|
-
|
|
3702
|
-
const
|
|
3703
|
-
if (
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
for (
|
|
3707
|
-
const
|
|
3708
|
-
if (
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
4148
|
+
function modelHasNewFields(model, ctx) {
|
|
4149
|
+
if (!ctx.apiSurface?.interfaces && !ctx.apiSurface?.typeAliases) return true;
|
|
4150
|
+
const domainName = resolveInterfaceName(model.name, ctx);
|
|
4151
|
+
if (ctx.apiSurface?.typeAliases?.[domainName]) return false;
|
|
4152
|
+
const baseline = ctx.apiSurface?.interfaces?.[domainName];
|
|
4153
|
+
if (!baseline?.fields) return true;
|
|
4154
|
+
for (const field of model.fields) {
|
|
4155
|
+
const camelName = fieldName$6(field.name);
|
|
4156
|
+
if (!baseline.fields[camelName]) return true;
|
|
4157
|
+
}
|
|
4158
|
+
return false;
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Return operations in a service that are NOT covered by existing hand-written
|
|
4162
|
+
* service classes.
|
|
4163
|
+
*/
|
|
4164
|
+
function uncoveredOperations(service, ctx) {
|
|
4165
|
+
const overlay = ctx.overlayLookup?.methodByOperation;
|
|
4166
|
+
if (!overlay || overlay.size === 0) return service.operations;
|
|
4167
|
+
const baselineClasses = ctx.apiSurface?.classes;
|
|
4168
|
+
if (!baselineClasses) return service.operations;
|
|
4169
|
+
const existingClassNames = new Set(Object.keys(baselineClasses));
|
|
4170
|
+
return service.operations.filter((op) => {
|
|
4171
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
4172
|
+
const match = overlay.get(httpKey);
|
|
4173
|
+
if (!match) return true;
|
|
4174
|
+
return !existingClassNames.has(match.className);
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
4177
|
+
/**
|
|
4178
|
+
* Compute the set of model names reachable from non-event service operations.
|
|
4179
|
+
*/
|
|
4180
|
+
function computeNonEventReachable(services, models) {
|
|
4181
|
+
const seeds = /* @__PURE__ */ new Set();
|
|
4182
|
+
for (const svc of services) {
|
|
4183
|
+
if (svc.name.toLowerCase() === "events") continue;
|
|
4184
|
+
for (const op of svc.operations) {
|
|
4185
|
+
const collectFromRef = (t) => {
|
|
4186
|
+
if (!t) return;
|
|
4187
|
+
if (t.kind === "model") seeds.add(t.name);
|
|
4188
|
+
if (t.kind === "array") collectFromRef(t.items);
|
|
4189
|
+
if (t.kind === "nullable") collectFromRef(t.inner);
|
|
4190
|
+
if (t.kind === "union") t.variants.forEach(collectFromRef);
|
|
4191
|
+
};
|
|
4192
|
+
collectFromRef(op.response);
|
|
4193
|
+
collectFromRef(op.requestBody);
|
|
4194
|
+
if (op.pagination?.itemType) collectFromRef(op.pagination.itemType);
|
|
3715
4195
|
}
|
|
3716
4196
|
}
|
|
3717
|
-
|
|
3718
|
-
const
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
4197
|
+
const modelMap = new Map(models.map((m) => [m.name, m]));
|
|
4198
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
4199
|
+
const queue = [...seeds];
|
|
4200
|
+
while (queue.length > 0) {
|
|
4201
|
+
const name = queue.pop();
|
|
4202
|
+
if (reachable.has(name)) continue;
|
|
4203
|
+
reachable.add(name);
|
|
4204
|
+
const m = modelMap.get(name);
|
|
4205
|
+
if (!m) continue;
|
|
4206
|
+
for (const field of m.fields) {
|
|
4207
|
+
const walk = (t) => {
|
|
4208
|
+
if (t.kind === "model" && !reachable.has(t.name)) queue.push(t.name);
|
|
4209
|
+
if (t.kind === "array") walk(t.items);
|
|
4210
|
+
if (t.kind === "nullable") walk(t.inner);
|
|
4211
|
+
if (t.kind === "union") t.variants.forEach(walk);
|
|
4212
|
+
};
|
|
4213
|
+
walk(field.type);
|
|
4214
|
+
}
|
|
3722
4215
|
}
|
|
3723
|
-
return
|
|
4216
|
+
return reachable;
|
|
3724
4217
|
}
|
|
3725
4218
|
//#endregion
|
|
3726
4219
|
//#region src/node/enums.ts
|
|
@@ -3732,13 +4225,15 @@ function generateEnums$7(enums, ctx) {
|
|
|
3732
4225
|
const files = [];
|
|
3733
4226
|
for (const enumDef of enums) {
|
|
3734
4227
|
if (isInlineEnum(enumDef.name)) continue;
|
|
3735
|
-
const
|
|
4228
|
+
const service = enumToService.get(enumDef.name);
|
|
4229
|
+
const dirName = resolveDir(service);
|
|
3736
4230
|
const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
|
|
3737
4231
|
const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
|
|
3738
4232
|
const generatedPath = `src/${dirName}/interfaces/${fileName$3(enumDef.name)}.interface.ts`;
|
|
3739
4233
|
const baselineSourceFile = baselineEnum?.sourceFile ?? baselineAlias?.sourceFile ?? liveSurfaceInterfacePath(enumDef.name);
|
|
3740
4234
|
if (dirName === "common" && !baselineSourceFile && (ctx.outputDir || ctx.targetDir || ctx.apiSurface)) continue;
|
|
3741
|
-
|
|
4235
|
+
const isOwnedEnumService = isNodeOwnedService(ctx, service, service ? serviceNameMap.get(service) : void 0);
|
|
4236
|
+
if (baselineSourceFile && baselineSourceFile !== generatedPath && !isOwnedEnumService) continue;
|
|
3742
4237
|
const lines = [];
|
|
3743
4238
|
let hasNewValues = false;
|
|
3744
4239
|
if (baselineEnum?.members) {
|
|
@@ -3824,7 +4319,7 @@ function assignEnumsToServices(enums, services, models = [], ctx) {
|
|
|
3824
4319
|
for (const name of refs) if (enumNames.has(name) && !enumToService.has(name)) enumToService.set(name, service.name);
|
|
3825
4320
|
}
|
|
3826
4321
|
if (models.length > 0) {
|
|
3827
|
-
const modelToService =
|
|
4322
|
+
const modelToService = assignModelsToEmittableServices(models, services, ctx);
|
|
3828
4323
|
for (const model of models) {
|
|
3829
4324
|
const service = modelToService.get(model.name);
|
|
3830
4325
|
if (!service) continue;
|
|
@@ -3834,25 +4329,6 @@ function assignEnumsToServices(enums, services, models = [], ctx) {
|
|
|
3834
4329
|
return enumToService;
|
|
3835
4330
|
}
|
|
3836
4331
|
//#endregion
|
|
3837
|
-
//#region src/node/options.ts
|
|
3838
|
-
function nodeOptions(ctx) {
|
|
3839
|
-
return ctx.emitterOptions ?? {};
|
|
3840
|
-
}
|
|
3841
|
-
function normalizeServiceName(name) {
|
|
3842
|
-
return name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
|
3843
|
-
}
|
|
3844
|
-
function ownedLookupNames(name) {
|
|
3845
|
-
const names = [name];
|
|
3846
|
-
if (name.startsWith("__baseline_dir__:")) names.push(name.slice(17));
|
|
3847
|
-
return names;
|
|
3848
|
-
}
|
|
3849
|
-
function isNodeOwnedService(ctx, ...names) {
|
|
3850
|
-
const configured = nodeOptions(ctx).ownedServices ?? [];
|
|
3851
|
-
if (configured.length === 0) return false;
|
|
3852
|
-
const owned = new Set(configured.map(normalizeServiceName));
|
|
3853
|
-
return names.some((name) => name !== void 0 ? ownedLookupNames(name).some((candidate) => owned.has(normalizeServiceName(candidate))) : false);
|
|
3854
|
-
}
|
|
3855
|
-
//#endregion
|
|
3856
4332
|
//#region src/node/field-plan.ts
|
|
3857
4333
|
/**
|
|
3858
4334
|
* Decide whether a `deserialize${X}` / `serialize${X}` helper will be
|
|
@@ -4718,7 +5194,7 @@ function ignoredResourceMethodNames(ctx, resourcePath) {
|
|
|
4718
5194
|
}
|
|
4719
5195
|
const methods = /* @__PURE__ */ new Set();
|
|
4720
5196
|
for (const block of content.matchAll(/@oagen-ignore-start[\s\S]*?@oagen-ignore-end/g)) for (const line of block[0].split("\n")) {
|
|
4721
|
-
const match = line.match(/^\s{2}(?:(?:public|private|protected)\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s
|
|
5197
|
+
const match = line.match(/^\s{2}(?:(?:public|private|protected)\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*[<(]/);
|
|
4722
5198
|
if (match) methods.add(match[1]);
|
|
4723
5199
|
}
|
|
4724
5200
|
return methods;
|
|
@@ -4780,6 +5256,17 @@ function optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resol
|
|
|
4780
5256
|
function renderOptionsParam(param) {
|
|
4781
5257
|
return `options${param.optional ? "?" : ""}: ${param.type}`;
|
|
4782
5258
|
}
|
|
5259
|
+
/**
|
|
5260
|
+
* Whether a baseline-derived type reference is a plain TypeScript identifier
|
|
5261
|
+
* that can appear in a named import. Baseline (live-SDK) method params can
|
|
5262
|
+
* carry inline object-literal TYPES (e.g. `{ intent: GenerateLinkIntent; ... }`);
|
|
5263
|
+
* treating that literal text as a type NAME would slugify it into a filename
|
|
5264
|
+
* and emit a named import of a brace-expression — both invalid. Literal types
|
|
5265
|
+
* are kept inline in the emitted signature instead and never imported.
|
|
5266
|
+
*/
|
|
5267
|
+
function isValidTypeIdentifier(name) {
|
|
5268
|
+
return /^[A-Za-z_$][\w$]*$/.test(name);
|
|
5269
|
+
}
|
|
4783
5270
|
function autoPaginatableItemType$1(returnType) {
|
|
4784
5271
|
return returnType?.match(/\b(?:AutoPaginatable|List)<\s*([A-Za-z_$][\w$]*)/)?.[1];
|
|
4785
5272
|
}
|
|
@@ -5108,6 +5595,7 @@ function generateResourceClass(service, ctx) {
|
|
|
5108
5595
|
if (hasIdempotentPost || hasCustomEncoding) lines.push("import type { PostOptions } from '../common/interfaces/post-options.interface';");
|
|
5109
5596
|
const importedTypeNames = /* @__PURE__ */ new Set();
|
|
5110
5597
|
for (const optionType of optionObjectTypes) {
|
|
5598
|
+
if (!isValidTypeIdentifier(optionType)) continue;
|
|
5111
5599
|
if (importedTypeNames.has(optionType)) continue;
|
|
5112
5600
|
importedTypeNames.add(optionType);
|
|
5113
5601
|
const sourceFile = baselineTypeSourceFile(ctx, optionType);
|
|
@@ -5546,7 +6034,8 @@ function renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelM
|
|
|
5546
6034
|
if (!itemType) return false;
|
|
5547
6035
|
const wireType = wireInterfaceName(itemType);
|
|
5548
6036
|
const needsWireSerializer = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name)).some((p) => fieldName$6(p.name) !== wireFieldName(p.name));
|
|
5549
|
-
const
|
|
6037
|
+
const restOptionsType = pathBindings.length > 0 ? `Omit<${optionParam.type}, ${pathBindings.map((b) => `'${b}'`).join(" | ")}>` : optionParam.type;
|
|
6038
|
+
const paginationType = needsWireSerializer ? "PaginationOptions" : restOptionsType;
|
|
5550
6039
|
const returnType = needsWireSerializer ? `Promise<AutoPaginatable<${itemType}, ${paginationType}>>` : preferredBaselineReturnType(ctx, baselineMethod?.returnType) ?? `Promise<AutoPaginatable<${itemType}, ${paginationType}>>`;
|
|
5551
6040
|
const listOptionsExpr = needsWireSerializer ? `options ? serialize${optionParam.type}(options) : undefined` : "paginationOptions";
|
|
5552
6041
|
lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${returnType} {`);
|
|
@@ -5641,7 +6130,8 @@ function renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelM
|
|
|
5641
6130
|
const finalReturnExpr = override?.returnDataProperty ? `${returnExpr}.${override.returnDataProperty}` : returnExpr;
|
|
5642
6131
|
lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
|
|
5643
6132
|
renderOptionsObjectDestructure(lines, pathBindings);
|
|
5644
|
-
|
|
6133
|
+
const emptyBodyArg = httpMethodNeedsBody(op.httpMethod) ? ", {}" : "";
|
|
6134
|
+
lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}${emptyBodyArg}${queryOptionsArg});`);
|
|
5645
6135
|
if (override?.returnExpression) {
|
|
5646
6136
|
lines.push(` const result = ${returnExpr};`);
|
|
5647
6137
|
lines.push(` return ${override.returnExpression};`);
|
|
@@ -6509,10 +6999,11 @@ function generateModels$7(models, ctx, shared) {
|
|
|
6509
6999
|
if (unresolvableNames.has(name)) continue;
|
|
6510
7000
|
const irEnumName = resolvedEnumNames.get(name);
|
|
6511
7001
|
if (irEnumName && !deps.enums.has(irEnumName)) {
|
|
6512
|
-
const
|
|
7002
|
+
const eService = enumToService.get(irEnumName);
|
|
7003
|
+
const eDir = resolveDir(eService);
|
|
6513
7004
|
const bEnum = ctx.apiSurface?.enums?.[irEnumName];
|
|
6514
7005
|
const bAlias = ctx.apiSurface?.typeAliases?.[irEnumName];
|
|
6515
|
-
const bSrc = bEnum?.sourceFile ?? bAlias?.sourceFile;
|
|
7006
|
+
const bSrc = isNodeOwnedService(ctx, eService) ? void 0 : bEnum?.sourceFile ?? bAlias?.sourceFile;
|
|
6516
7007
|
const gPath = `src/${eDir}/interfaces/${fileName$3(irEnumName)}.interface.ts`;
|
|
6517
7008
|
const cPath = `src/${dirName}/interfaces/${fileName$3(model.name)}.interface.ts`;
|
|
6518
7009
|
if (bSrc === cPath) {
|
|
@@ -6556,8 +7047,9 @@ function generateModels$7(models, ctx, shared) {
|
|
|
6556
7047
|
if (isInlineEnum(dep)) continue;
|
|
6557
7048
|
const baselineEnum = ctx.apiSurface?.enums?.[dep];
|
|
6558
7049
|
const baselineAlias = ctx.apiSurface?.typeAliases?.[dep];
|
|
6559
|
-
const
|
|
6560
|
-
const
|
|
7050
|
+
const depService = enumToService.get(dep);
|
|
7051
|
+
const baselineSrc = isNodeOwnedService(ctx, depService) ? void 0 : baselineEnum?.sourceFile ?? baselineAlias?.sourceFile ?? liveSurfaceInterfacePath(dep);
|
|
7052
|
+
const depDir = resolveDir(depService);
|
|
6561
7053
|
const generatedPath = `src/${depDir}/interfaces/${fileName$3(dep)}.interface.ts`;
|
|
6562
7054
|
const currentFilePath = `src/${dirName}/interfaces/${fileName$3(model.name)}.interface.ts`;
|
|
6563
7055
|
if (baselineSrc === currentFilePath) {
|
|
@@ -6648,6 +7140,7 @@ function generateModels$7(models, ctx, shared) {
|
|
|
6648
7140
|
if (generatedNames.has(name)) continue;
|
|
6649
7141
|
const sepPath = `src/${dirName}/interfaces/${fileName$3(name)}.interface.ts`;
|
|
6650
7142
|
if (sepPath !== filePath && files.some((f) => f.path === sepPath)) continue;
|
|
7143
|
+
if (!isInlineEnum(name) && ctx.spec.enums.some((e) => e.name === name) && isNodeOwnedService(ctx, enumToService.get(name))) continue;
|
|
6651
7144
|
inlineNames.add(name);
|
|
6652
7145
|
}
|
|
6653
7146
|
};
|
|
@@ -7020,6 +7513,7 @@ function baselineTypeResolvable(typeStr, importableNames) {
|
|
|
7020
7513
|
return true;
|
|
7021
7514
|
}
|
|
7022
7515
|
function baselineFieldCompatible(baselineField, irField) {
|
|
7516
|
+
if (baselineTypeIsDegradedAny(baselineField.type) && hasNamedTypeReference(irField.type)) return false;
|
|
7023
7517
|
const irNullable = irField.type.kind === "nullable";
|
|
7024
7518
|
const baselineHasNull = baselineField.type.includes("null");
|
|
7025
7519
|
if (irNullable && !baselineHasNull && irField.required) return false;
|
|
@@ -7027,6 +7521,21 @@ function baselineFieldCompatible(baselineField, irField) {
|
|
|
7027
7521
|
if (baselineField.type === "Record<string, unknown>" && hasSpecificIRType(irField.type)) return false;
|
|
7028
7522
|
return true;
|
|
7029
7523
|
}
|
|
7524
|
+
/** `any`, `any[]`, `any | null`, … — shapes api-surface extraction degrades to. */
|
|
7525
|
+
function baselineTypeIsDegradedAny(typeStr) {
|
|
7526
|
+
return typeStr.replace(/\s*\|\s*(?:null|undefined)\b/g, "").replace(/\[\]$/, "").trim() === "any";
|
|
7527
|
+
}
|
|
7528
|
+
/** Does the IR type reference a named model/enum anywhere (incl. arrays)? */
|
|
7529
|
+
function hasNamedTypeReference(ref) {
|
|
7530
|
+
switch (ref.kind) {
|
|
7531
|
+
case "model":
|
|
7532
|
+
case "enum": return true;
|
|
7533
|
+
case "array": return hasNamedTypeReference(ref.items);
|
|
7534
|
+
case "nullable": return hasNamedTypeReference(ref.inner);
|
|
7535
|
+
case "union": return ref.variants.some((v) => hasNamedTypeReference(v));
|
|
7536
|
+
default: return false;
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
7030
7539
|
function hasSpecificIRType(ref) {
|
|
7031
7540
|
switch (ref.kind) {
|
|
7032
7541
|
case "model":
|
|
@@ -8841,6 +9350,22 @@ function getEmittedPaths(ctx) {
|
|
|
8841
9350
|
}
|
|
8842
9351
|
return set;
|
|
8843
9352
|
}
|
|
9353
|
+
/**
|
|
9354
|
+
* Every `GeneratedFile` the node emitter has produced so far in this ctx,
|
|
9355
|
+
* keyed by path. All emitter hooks share one ctx (and the engine only reads
|
|
9356
|
+
* file contents after the last hook returns), so the final hook can run a
|
|
9357
|
+
* whole-run pass over files emitted by earlier hooks — see
|
|
9358
|
+
* `enforceEmittedImportInvariant`.
|
|
9359
|
+
*/
|
|
9360
|
+
const emittedFilesCache = /* @__PURE__ */ new WeakMap();
|
|
9361
|
+
function getEmittedFiles(ctx) {
|
|
9362
|
+
let map = emittedFilesCache.get(ctx);
|
|
9363
|
+
if (!map) {
|
|
9364
|
+
map = /* @__PURE__ */ new Map();
|
|
9365
|
+
emittedFilesCache.set(ctx, map);
|
|
9366
|
+
}
|
|
9367
|
+
return map;
|
|
9368
|
+
}
|
|
8844
9369
|
function getSurface(ctx) {
|
|
8845
9370
|
let surface = surfaceCache.get(ctx);
|
|
8846
9371
|
if (surface) return surface;
|
|
@@ -8861,6 +9386,9 @@ function getSurface(ctx) {
|
|
|
8861
9386
|
if (ifaces) for (const name of Object.keys(ifaces)) allInterfaces.add(name);
|
|
8862
9387
|
for (const name of surface.interfaces.keys()) allInterfaces.add(name);
|
|
8863
9388
|
setBaselineInterfaceNames(allInterfaces);
|
|
9389
|
+
const declaredNames = new Set(allInterfaces);
|
|
9390
|
+
for (const name of Object.keys(ctx.apiSurface?.typeAliases ?? {})) declaredNames.add(name);
|
|
9391
|
+
setBaselineDeclaredNames(declaredNames);
|
|
8864
9392
|
setInlineEnumUnions(/* @__PURE__ */ new Map());
|
|
8865
9393
|
setAdoptedModelNames(computeAdoptedModelNames(ctx, surface));
|
|
8866
9394
|
const renamed = /* @__PURE__ */ new Set();
|
|
@@ -9026,6 +9554,15 @@ function isOwnedPath(relPath, policy) {
|
|
|
9026
9554
|
const dir = topLevelDir(relPath);
|
|
9027
9555
|
return dir !== void 0 && policy.ownedServiceDirs.has(dir);
|
|
9028
9556
|
}
|
|
9557
|
+
/** Read the current on-disk content of a live-surface file, if present. */
|
|
9558
|
+
function readExistingSurfaceFile(surface, relPath) {
|
|
9559
|
+
if (!surface.rootDir) return null;
|
|
9560
|
+
try {
|
|
9561
|
+
return fs$1.readFileSync(path$1.join(surface.rootDir, relPath), "utf8");
|
|
9562
|
+
} catch {
|
|
9563
|
+
return null;
|
|
9564
|
+
}
|
|
9565
|
+
}
|
|
9029
9566
|
function extractRelativeImportPaths(content, fromPath) {
|
|
9030
9567
|
const dir = path$1.dirname(fromPath);
|
|
9031
9568
|
const paths = [];
|
|
@@ -9072,6 +9609,15 @@ function applyLiveSurface(files, ctx, surface) {
|
|
|
9072
9609
|
if (isManagedDir && !policy.regenerateOwnedTests) continue;
|
|
9073
9610
|
}
|
|
9074
9611
|
if (surface.autogenFiles.has(f.path) || ownedPath) {
|
|
9612
|
+
const dir = topLevelDir(f.path);
|
|
9613
|
+
const isAdoptedDirPath = dir !== void 0 && policy.adoptedServiceDirs.has(dir);
|
|
9614
|
+
if (!ownedPath && !isAdoptedDirPath && surface.autogenFiles.has(f.path)) {
|
|
9615
|
+
const existingText = readExistingSurfaceFile(surface, f.path);
|
|
9616
|
+
if (existingText !== null) {
|
|
9617
|
+
const merged = mergeGeneratedClassMethodsIntoExisting(existingText, f.content);
|
|
9618
|
+
if (merged !== null) f.content = merged;
|
|
9619
|
+
}
|
|
9620
|
+
}
|
|
9075
9621
|
f.overwriteExisting = true;
|
|
9076
9622
|
f.skipIfExists = false;
|
|
9077
9623
|
}
|
|
@@ -9079,10 +9625,136 @@ function applyLiveSurface(files, ctx, surface) {
|
|
|
9079
9625
|
out.push(f);
|
|
9080
9626
|
}
|
|
9081
9627
|
const emitted = getEmittedPaths(ctx);
|
|
9082
|
-
|
|
9628
|
+
const emittedFiles = getEmittedFiles(ctx);
|
|
9629
|
+
for (const f of out) {
|
|
9630
|
+
emitted.add(f.path);
|
|
9631
|
+
emittedFiles.set(f.path, f);
|
|
9632
|
+
}
|
|
9083
9633
|
return out;
|
|
9084
9634
|
}
|
|
9085
9635
|
/**
|
|
9636
|
+
* Matches single-line `import`/`export … from './relative'` statements — the
|
|
9637
|
+
* only form the node emitter produces. Captures: keyword, optional ` type`
|
|
9638
|
+
* modifier, the binding clause (`* [as ns]` or `{ … }`), and the module path.
|
|
9639
|
+
*/
|
|
9640
|
+
const RELATIVE_FROM_STMT_RE = /^(import|export)(\s+type)?\s+(\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s+from\s+['"](\.[^'"]+)['"];?\s*$/;
|
|
9641
|
+
const EXPORTED_DECL_RE = /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:interface|class|enum|function|const|let|var|type)\s+([A-Za-z_$][\w$]*)/gm;
|
|
9642
|
+
const EXPORTED_CLAUSE_RE = /^export\s+(?:type\s+)?\{([^}]*)\}/gm;
|
|
9643
|
+
/** Repo-relative paths a relative import specifier may resolve to. */
|
|
9644
|
+
function importTargetCandidates(fromPath, spec) {
|
|
9645
|
+
const base = path$1.posix.normalize(path$1.posix.join(path$1.posix.dirname(fromPath), spec));
|
|
9646
|
+
return [
|
|
9647
|
+
base,
|
|
9648
|
+
`${base}.ts`,
|
|
9649
|
+
`${base}/index.ts`
|
|
9650
|
+
];
|
|
9651
|
+
}
|
|
9652
|
+
/** Index exported symbol → file path across this run's emitted contents. */
|
|
9653
|
+
function indexEmittedExports(files) {
|
|
9654
|
+
const index = /* @__PURE__ */ new Map();
|
|
9655
|
+
for (const f of files) {
|
|
9656
|
+
if (!f.path.endsWith(".ts") || !f.content) continue;
|
|
9657
|
+
for (const m of f.content.matchAll(EXPORTED_DECL_RE)) if (!index.has(m[1])) index.set(m[1], f.path);
|
|
9658
|
+
for (const m of f.content.matchAll(EXPORTED_CLAUSE_RE)) for (const raw of m[1].split(",")) {
|
|
9659
|
+
const entry = raw.trim();
|
|
9660
|
+
if (!entry) continue;
|
|
9661
|
+
const parts = entry.split(/\s+as\s+/);
|
|
9662
|
+
const exported = (parts[1] ?? parts[0]).replace(/^type\s+/, "").trim();
|
|
9663
|
+
if (exported && !index.has(exported)) index.set(exported, f.path);
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
return index;
|
|
9667
|
+
}
|
|
9668
|
+
/**
|
|
9669
|
+
* Enforce: every relative import/re-export path in emitted code resolves to
|
|
9670
|
+
* either (i) a file emitted in the same run, or (ii) a file that already
|
|
9671
|
+
* exists on disk in the target SDK.
|
|
9672
|
+
*
|
|
9673
|
+
* Violations observed in real generations (all TS2307 in otherwise-valid
|
|
9674
|
+
* output): serializer imports pointing at canonical paths while the function
|
|
9675
|
+
* lives in a legacy hand serializer under a different filename; barrels
|
|
9676
|
+
* re-exporting a module-local enum file whose declaration lives under
|
|
9677
|
+
* `src/common/interfaces`; barrels exporting interface files that no run
|
|
9678
|
+
* emits at all.
|
|
9679
|
+
*
|
|
9680
|
+
* Repair strategy, per statement whose target is neither emitted nor on
|
|
9681
|
+
* disk:
|
|
9682
|
+
* 1. Locate each imported symbol (this run's emissions first, then the
|
|
9683
|
+
* live-surface declaration maps) and rewrite the path to where the
|
|
9684
|
+
* symbol actually lives — splitting into one statement per location
|
|
9685
|
+
* when symbols are spread across files.
|
|
9686
|
+
* 2. `export * from` / namespace imports carry no symbol list, so derive
|
|
9687
|
+
* the expected symbol from the file stem (`foo-bar.interface` →
|
|
9688
|
+
* `FooBar`, `foo.serializer` → `deserializeFoo`/`serializeFoo`).
|
|
9689
|
+
* 3. When a symbol exists nowhere, drop the statement and warn — a missing
|
|
9690
|
+
* named export fails loudly at the usage site instead of as a phantom
|
|
9691
|
+
* module, and a barrel line for a never-emitted file is pure noise.
|
|
9692
|
+
*
|
|
9693
|
+
* Mutates `f.content` in place and returns the warnings issued.
|
|
9694
|
+
*/
|
|
9695
|
+
function enforceEmittedImportInvariant(files, emittedPaths, surface) {
|
|
9696
|
+
const fileList = [...files];
|
|
9697
|
+
const emittedSymbols = indexEmittedExports(fileList);
|
|
9698
|
+
const warnings = [];
|
|
9699
|
+
const targetExists = (fromPath, spec) => importTargetCandidates(fromPath, spec).some((p) => emittedPaths.has(p) || surface.files.has(p));
|
|
9700
|
+
const locateSymbol = (name) => emittedSymbols.get(name) ?? surface.functions.get(name) ?? surface.interfaces.get(name)?.filePath ?? surface.classes.get(name)?.filePath;
|
|
9701
|
+
for (const f of fileList) {
|
|
9702
|
+
if (!f.path.endsWith(".ts") || !f.content) continue;
|
|
9703
|
+
let changed = false;
|
|
9704
|
+
const outLines = [];
|
|
9705
|
+
for (const line of f.content.split("\n")) {
|
|
9706
|
+
const m = line.match(RELATIVE_FROM_STMT_RE);
|
|
9707
|
+
if (!m || targetExists(f.path, m[4])) {
|
|
9708
|
+
outLines.push(line);
|
|
9709
|
+
continue;
|
|
9710
|
+
}
|
|
9711
|
+
const [, keyword, typeMod, clause, spec] = m;
|
|
9712
|
+
const repaired = repairUnresolvableStatement(f.path, keyword, typeMod ?? "", clause, spec, locateSymbol);
|
|
9713
|
+
changed = true;
|
|
9714
|
+
outLines.push(...repaired.lines);
|
|
9715
|
+
if (repaired.warning) warnings.push(repaired.warning);
|
|
9716
|
+
}
|
|
9717
|
+
if (changed) f.content = outLines.join("\n");
|
|
9718
|
+
}
|
|
9719
|
+
for (const w of warnings) console.warn(w);
|
|
9720
|
+
return warnings;
|
|
9721
|
+
}
|
|
9722
|
+
function repairUnresolvableStatement(fromPath, keyword, typeMod, clause, spec, locateSymbol) {
|
|
9723
|
+
if (clause.startsWith("{")) {
|
|
9724
|
+
const entries = clause.slice(1, -1).split(",").map((e) => e.trim()).filter(Boolean);
|
|
9725
|
+
const byLocation = /* @__PURE__ */ new Map();
|
|
9726
|
+
const missing = [];
|
|
9727
|
+
for (const entry of entries) {
|
|
9728
|
+
const source = entry.split(/\s+as\s+/)[0].replace(/^type\s+/, "").trim();
|
|
9729
|
+
const location = locateSymbol(source);
|
|
9730
|
+
if (!location) {
|
|
9731
|
+
missing.push(source);
|
|
9732
|
+
continue;
|
|
9733
|
+
}
|
|
9734
|
+
if (location === fromPath) continue;
|
|
9735
|
+
const group = byLocation.get(location);
|
|
9736
|
+
if (group) group.push(entry);
|
|
9737
|
+
else byLocation.set(location, [entry]);
|
|
9738
|
+
}
|
|
9739
|
+
return {
|
|
9740
|
+
lines: [...byLocation].map(([location, group]) => `${keyword}${typeMod} { ${group.join(", ")} } from '${relativeImport(fromPath, location)}';`),
|
|
9741
|
+
warning: missing.length > 0 ? `oagen(node): dropped unresolvable symbol(s) from ${keyword} in ${fromPath}: '${spec}' — found neither in this run's output nor in the target SDK: ${missing.join(", ")}` : void 0
|
|
9742
|
+
};
|
|
9743
|
+
}
|
|
9744
|
+
const stem = spec.split("/").pop() ?? "";
|
|
9745
|
+
let location;
|
|
9746
|
+
if (stem.endsWith(".interface")) location = locateSymbol(toPascalCase(stem.slice(0, -10)));
|
|
9747
|
+
else if (stem.endsWith(".serializer")) {
|
|
9748
|
+
const base = toPascalCase(stem.slice(0, -11));
|
|
9749
|
+
location = locateSymbol(`deserialize${base}`) ?? locateSymbol(`serialize${base}`);
|
|
9750
|
+
}
|
|
9751
|
+
if (location && location !== fromPath) return { lines: [`${keyword}${typeMod} ${clause} from '${relativeImport(fromPath, location)}';`] };
|
|
9752
|
+
return {
|
|
9753
|
+
lines: [],
|
|
9754
|
+
warning: `oagen(node): dropped unresolvable ${keyword} in ${fromPath}: '${spec}' — module is neither emitted this run nor present in the target SDK`
|
|
9755
|
+
};
|
|
9756
|
+
}
|
|
9757
|
+
/**
|
|
9086
9758
|
* Re-declare prior-manifest paths that we did not emit this run so manifest
|
|
9087
9759
|
* pruning can tell "intentionally removed" from "untouched but still managed."
|
|
9088
9760
|
*
|
|
@@ -9193,7 +9865,9 @@ const nodeEmitter = {
|
|
|
9193
9865
|
generateTests(spec, ctx) {
|
|
9194
9866
|
const nodeCtx = withNodeOperationOverrides(ctx);
|
|
9195
9867
|
const surface = getSurface(nodeCtx);
|
|
9196
|
-
|
|
9868
|
+
const result = [...nodeOptions(nodeCtx).regenerateOwnedTests ? applyLiveSurface(generateTests$7(spec, nodeCtx), nodeCtx, surface) : [], ...carryForwardManagedFiles(nodeCtx, surface)];
|
|
9869
|
+
if (managedPathsFor(nodeCtx, surface).size > 0) enforceEmittedImportInvariant(getEmittedFiles(nodeCtx).values(), getEmittedPaths(nodeCtx), surface);
|
|
9870
|
+
return result;
|
|
9197
9871
|
},
|
|
9198
9872
|
buildOperationsMap() {
|
|
9199
9873
|
return {};
|
|
@@ -28470,4 +29144,4 @@ const workosEmittersPlugin = {
|
|
|
28470
29144
|
//#endregion
|
|
28471
29145
|
export { fieldName$2 as A, servicePropertyName$2 as B, apiClassName as C, dotnetEmitter as D, propertyName as E, fieldName$3 as F, fieldName$5 as H, methodName$3 as I, trimMountedResourceFromMethod$2 as L, trimMountedResourceFromMethod$1 as M, goEmitter as N, appendAsyncSuffix as O, className$3 as P, phpEmitter as R, kotlinEmitter as S, packageSegment as T, safeParamName$1 as U, pythonEmitter as V, nodeEmitter as W, rubyEmitter as _, rustExtractor as a, resolveServiceTarget as b, pythonExtractor as c, rustEmitter as d, fieldName as f, typeName as g, resourceAccessorName as h, kotlinExtractor as i, methodName$2 as j, className$2 as k, rubyExtractor as l, moduleName as m, elixirExtractor as n, goExtractor as o, methodName as p, dotnetExtractor as r, phpExtractor as s, workosEmittersPlugin as t, nodeExtractor as u, buildExportedClassNameSet as v, methodName$1 as w, safeParamName as x, fieldName$1 as y, fieldName$4 as z };
|
|
28472
29146
|
|
|
28473
|
-
//# sourceMappingURL=plugin-
|
|
29147
|
+
//# sourceMappingURL=plugin-CpO8rePT.mjs.map
|