@workos/oagen 0.22.7 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/index.mjs +207 -33
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +2 -2
- package/dist/compat/index.mjs +7 -7
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-D4XaiPeA.mjs → diff-B7dMUCoJ.mjs} +128 -19
- package/dist/diff-B7dMUCoJ.mjs.map +1 -0
- package/dist/{differ-Q13KY9Rd.d.mts → differ-DMmw0tbA.d.mts} +5 -5
- package/dist/{differ-Q13KY9Rd.d.mts.map → differ-DMmw0tbA.d.mts.map} +1 -1
- package/dist/index.d.mts +50 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +74 -11
- package/dist/index.mjs.map +1 -1
- package/dist/{overlay-cb32jDBS.mjs → overlay-sQHQoN0I.mjs} +4 -4
- package/dist/overlay-sQHQoN0I.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.mjs +73 -10
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/scripts/smoke/sdk-test.mjs +3 -3
- package/dist/scripts/smoke/sdk-test.mjs.map +1 -1
- package/dist/smoke.d.mts +3 -4
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +73 -10
- package/dist/smoke.mjs.map +1 -1
- package/dist/{types-DZrKFHis.d.mts → types-nM5Kp7UP.d.mts} +7 -9
- package/dist/types-nM5Kp7UP.d.mts.map +1 -0
- package/dist/verify/index.d.mts +2 -2
- package/dist/verify/index.mjs +2 -2
- package/docs/architecture/extractor-contract.md +2 -2
- package/docs/architecture/pipeline.md +2 -2
- package/docs/cli.md +19 -19
- package/docs/core/compatibility-policy.md +1 -1
- package/docs/core/manifest-schema.md +2 -2
- package/package.json +1 -1
- package/skills/generate-emitter/SKILL.md +2 -2
- package/skills/generate-emitter/references/common-pitfalls.md +1 -1
- package/skills/generate-emitter/references/project-scaffold.md +3 -3
- package/skills/generate-extractor/SKILL.md +2 -2
- package/skills/generate-smoke-test/SKILL.md +1 -1
- package/skills/verify-smoke-test/SKILL.md +1 -1
- package/dist/diff-D4XaiPeA.mjs.map +0 -1
- package/dist/overlay-cb32jDBS.mjs.map +0 -1
- package/dist/types-DZrKFHis.d.mts.map +0 -1
|
@@ -715,12 +715,12 @@ function extractKotlinClassMembers(classBody, source) {
|
|
|
715
715
|
}
|
|
716
716
|
/**
|
|
717
717
|
* KDoc signature that identifies a generator-emitted service accessor inside
|
|
718
|
-
*
|
|
718
|
+
* a Kotlin client class. The Kotlin client emitter writes every accessor with this
|
|
719
719
|
* exact doc preamble — when the existing file carries a member with this doc
|
|
720
720
|
* but the regenerated content omits it, the member is stale (its mount target
|
|
721
721
|
* was remapped or removed) and gets pruned.
|
|
722
722
|
*/
|
|
723
|
-
const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[
|
|
723
|
+
const KOTLIN_MANAGED_ACCESSOR_DOC = /\/\*\*[\s\S]*?Lazily-constructed \[[^\]]+\] accessor for this \[[^\]]+\] client\.[\s\S]*?\*\//;
|
|
724
724
|
const kotlinMergeAdapter = {
|
|
725
725
|
language: "kotlin",
|
|
726
726
|
grammarModule: "tree-sitter-kotlin",
|
|
@@ -973,7 +973,6 @@ const nodeMergeAdapter = {
|
|
|
973
973
|
const newText = newMembers.map((m) => m.text).join("\n");
|
|
974
974
|
for (const match of newText.matchAll(/this\.(\w+)/g)) {
|
|
975
975
|
const propName = match[1];
|
|
976
|
-
if (propName === "workos") continue;
|
|
977
976
|
if (propName && !existingMemberKeys.has(propName)) return true;
|
|
978
977
|
}
|
|
979
978
|
return false;
|
|
@@ -2957,6 +2956,15 @@ function spliceExtraImports(existingLines, resultLines) {
|
|
|
2957
2956
|
//#endregion
|
|
2958
2957
|
//#region src/engine/generate-files.ts
|
|
2959
2958
|
function buildEmitterContext(spec, options) {
|
|
2959
|
+
const resolvedOperations = resolveOperations(spec, options.operationHints, options.mountRules);
|
|
2960
|
+
let scopedModelNames;
|
|
2961
|
+
let scopedEnumNames;
|
|
2962
|
+
if (options.scopedServices && options.scopedServices.size > 0) {
|
|
2963
|
+
const selected = options.scopedServices;
|
|
2964
|
+
const reachable = collectReferencedNames(spec.services.filter((s) => resolvedOperations.some((r) => r.service.name === s.name && selected.has(r.mountOn))), spec.models, { preserveAllDiscriminated: false });
|
|
2965
|
+
scopedModelNames = reachable.models;
|
|
2966
|
+
scopedEnumNames = reachable.enums;
|
|
2967
|
+
}
|
|
2960
2968
|
return {
|
|
2961
2969
|
namespace: toSnakeCase(options.namespace),
|
|
2962
2970
|
namespacePascal: options.namespace,
|
|
@@ -2964,11 +2972,14 @@ function buildEmitterContext(spec, options) {
|
|
|
2964
2972
|
outputDir: options.outputDir,
|
|
2965
2973
|
apiSurface: options.apiSurface,
|
|
2966
2974
|
overlayLookup: options.overlayLookup,
|
|
2967
|
-
resolvedOperations
|
|
2975
|
+
resolvedOperations,
|
|
2968
2976
|
modelHints: options.modelHints,
|
|
2969
2977
|
emitterOptions: options.emitterOptions,
|
|
2970
2978
|
targetDir: options.target,
|
|
2971
|
-
priorTargetManifestPaths: options.priorTargetManifestPaths
|
|
2979
|
+
priorTargetManifestPaths: options.priorTargetManifestPaths,
|
|
2980
|
+
scopedServices: options.scopedServices,
|
|
2981
|
+
scopedModelNames,
|
|
2982
|
+
scopedEnumNames
|
|
2972
2983
|
};
|
|
2973
2984
|
}
|
|
2974
2985
|
/**
|
|
@@ -2976,7 +2987,8 @@ function buildEmitterContext(spec, options) {
|
|
|
2976
2987
|
* Walks operation parameters, request bodies, responses, and pagination item
|
|
2977
2988
|
* types, then chases model field references until the set stabilizes.
|
|
2978
2989
|
*/
|
|
2979
|
-
function collectReferencedNames(services, models) {
|
|
2990
|
+
function collectReferencedNames(services, models, opts = {}) {
|
|
2991
|
+
const preserveAllDiscriminated = opts.preserveAllDiscriminated !== false;
|
|
2980
2992
|
const referencedModels = /* @__PURE__ */ new Set();
|
|
2981
2993
|
const referencedEnums = /* @__PURE__ */ new Set();
|
|
2982
2994
|
const collectFromTypeRef = (ref) => {
|
|
@@ -2998,7 +3010,7 @@ function collectReferencedNames(services, models) {
|
|
|
2998
3010
|
for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
|
|
2999
3011
|
if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
|
|
3000
3012
|
}
|
|
3001
|
-
for (const model of models) {
|
|
3013
|
+
if (preserveAllDiscriminated) for (const model of models) {
|
|
3002
3014
|
if (isDiscriminatedModel(model)) referencedModels.add(model.name);
|
|
3003
3015
|
const disc = model.discriminator;
|
|
3004
3016
|
if (disc?.mapping) for (const variantName of Object.values(disc.mapping)) referencedModels.add(variantName);
|
|
@@ -3012,6 +3024,13 @@ function collectReferencedNames(services, models) {
|
|
|
3012
3024
|
visited.add(name);
|
|
3013
3025
|
const model = modelsByName.get(name);
|
|
3014
3026
|
if (!model) continue;
|
|
3027
|
+
const disc = model.discriminator;
|
|
3028
|
+
if (disc?.mapping) {
|
|
3029
|
+
for (const variantName of Object.values(disc.mapping)) if (!referencedModels.has(variantName)) {
|
|
3030
|
+
referencedModels.add(variantName);
|
|
3031
|
+
queue.push(variantName);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3015
3034
|
for (const field of model.fields) {
|
|
3016
3035
|
collectFromTypeRef(field.type);
|
|
3017
3036
|
for (const m of referencedModels) if (!visited.has(m)) queue.push(m);
|
|
@@ -3025,7 +3044,16 @@ function collectReferencedNames(services, models) {
|
|
|
3025
3044
|
function isDiscriminatedModel(model) {
|
|
3026
3045
|
return model.fields.some((field) => (field.name === "event" || field.name === "type" || field.name === "object") && field.type.kind === "literal");
|
|
3027
3046
|
}
|
|
3028
|
-
/**
|
|
3047
|
+
/**
|
|
3048
|
+
* Collect all generated files from an emitter (no headers, no path prefixes).
|
|
3049
|
+
*
|
|
3050
|
+
* In a scoped (`--services`) run the spec is NOT filtered: every generator runs
|
|
3051
|
+
* over the full spec so model placement, dedup, the root client, and all
|
|
3052
|
+
* aggregate/barrel files are byte-identical to a full run (and a brand-new
|
|
3053
|
+
* selected service is wired into the client automatically). The emitters
|
|
3054
|
+
* themselves consult `ctx.scopedServices` to emit only the selected services'
|
|
3055
|
+
* per-service resource/test files.
|
|
3056
|
+
*/
|
|
3029
3057
|
function generateAllFiles(spec, emitter, ctx) {
|
|
3030
3058
|
const referenced = collectReferencedNames(spec.services, spec.models);
|
|
3031
3059
|
const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
|
|
@@ -3066,6 +3094,47 @@ function generateFiles(spec, emitter, options) {
|
|
|
3066
3094
|
};
|
|
3067
3095
|
}
|
|
3068
3096
|
//#endregion
|
|
3097
|
+
//#region src/engine/scoped-services.ts
|
|
3098
|
+
/**
|
|
3099
|
+
* Validate a `--services` selection and return it as a set of POST-MOUNT service
|
|
3100
|
+
* names for emitters to scope on.
|
|
3101
|
+
*
|
|
3102
|
+
* Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
|
|
3103
|
+
* shared-schema decisions are global computations over the full service/model set
|
|
3104
|
+
* (filtering them churns shared `common/` files). Instead the full spec flows to
|
|
3105
|
+
* the emitters unchanged and this set is passed as `ctx.scopedServices`; each
|
|
3106
|
+
* emitter gates only its per-service resource/test emission on it.
|
|
3107
|
+
*
|
|
3108
|
+
* Matching is on the POST-MOUNT name. Mount resolution never mutates
|
|
3109
|
+
* `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
|
|
3110
|
+
* (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
|
|
3111
|
+
* post-mount name of a source service is derived here as
|
|
3112
|
+
* `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
|
|
3113
|
+
* `DirectorySync`) is valid because every source tag that mounts into it
|
|
3114
|
+
* (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
|
|
3115
|
+
* emitters group resources by mount, so all siblings emit together.
|
|
3116
|
+
*
|
|
3117
|
+
* @param spec - The parsed API spec (never mutated; never filtered).
|
|
3118
|
+
* @param selected - Post-mount service names from `--services` (normalized/trimmed).
|
|
3119
|
+
* @param mountRules - Source-service-name → post-mount-target mappings.
|
|
3120
|
+
* @returns The validated selection as a `Set` of post-mount names.
|
|
3121
|
+
* @throws A `ConfigError` listing valid post-mount names when a selection is
|
|
3122
|
+
* unknown — scoped generation never silently emits the wrong set.
|
|
3123
|
+
*/
|
|
3124
|
+
function resolveScopedServices(spec, selected, mountRules) {
|
|
3125
|
+
const mounts = mountRules ?? {};
|
|
3126
|
+
const postMountName = (name) => mounts[name] ?? name;
|
|
3127
|
+
const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
|
|
3128
|
+
const selectedSet = new Set(selected);
|
|
3129
|
+
const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
|
|
3130
|
+
if (unknown.length > 0) {
|
|
3131
|
+
const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
|
|
3132
|
+
err.name = "ConfigError";
|
|
3133
|
+
throw err;
|
|
3134
|
+
}
|
|
3135
|
+
return selectedSet;
|
|
3136
|
+
}
|
|
3137
|
+
//#endregion
|
|
3069
3138
|
//#region src/engine/integrate.ts
|
|
3070
3139
|
/**
|
|
3071
3140
|
* Resolve a relative import path against a source file path to get the
|
|
@@ -3182,7 +3251,7 @@ function mapFilesForTargetIntegration(files, language) {
|
|
|
3182
3251
|
* that would shadow an existing module file (e.g., `api_keys.py`), move the
|
|
3183
3252
|
* module file's content into `_compat.py` inside the package and add a
|
|
3184
3253
|
* re-export from `__init__.py`. This preserves backward-compatible imports
|
|
3185
|
-
* (e.g., `from
|
|
3254
|
+
* (e.g., `from acme.api_keys import ApiKeysModule`) while the package directory
|
|
3186
3255
|
* provides the generated models and resource classes.
|
|
3187
3256
|
*/
|
|
3188
3257
|
async function resolveFileToDirectoryConflicts(files, targetDir) {
|
|
@@ -3348,6 +3417,35 @@ function computeStalePaths(prev, currentPaths) {
|
|
|
3348
3417
|
return prev.files.filter((p) => !current.has(p)).sort();
|
|
3349
3418
|
}
|
|
3350
3419
|
/**
|
|
3420
|
+
* Merge a scoped run's emitted records into the prior manifest's records.
|
|
3421
|
+
*
|
|
3422
|
+
* A scoped (`--services`) run only emits files for the selected services, but it
|
|
3423
|
+
* must NOT discard the prior manifest's record of unselected services' files
|
|
3424
|
+
* records. The result unions prior + scoped file paths and shallow-merges the
|
|
3425
|
+
* operations maps, with scoped operations overriding prior entries for the same
|
|
3426
|
+
* "METHOD /path" key (a re-emission updates that operation's record).
|
|
3427
|
+
*
|
|
3428
|
+
* When `prev` is null (the first-ever run is scoped) there is nothing to
|
|
3429
|
+
* preserve, so the scoped subset is returned unchanged — matching the
|
|
3430
|
+
* first-adoption behavior of a full run.
|
|
3431
|
+
*/
|
|
3432
|
+
function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
|
|
3433
|
+
const scopedFileList = [...scopedFiles];
|
|
3434
|
+
if (!prev) return {
|
|
3435
|
+
files: scopedFileList,
|
|
3436
|
+
operations: scopedOperations
|
|
3437
|
+
};
|
|
3438
|
+
const files = [...new Set([...prev.files, ...scopedFileList])].sort();
|
|
3439
|
+
const priorOps = prev.operations;
|
|
3440
|
+
return {
|
|
3441
|
+
files,
|
|
3442
|
+
operations: priorOps || scopedOperations ? {
|
|
3443
|
+
...priorOps,
|
|
3444
|
+
...scopedOperations
|
|
3445
|
+
} : void 0
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3351
3449
|
* Delete stale files from `dir`.
|
|
3352
3450
|
*
|
|
3353
3451
|
* When `header` is provided, a file is only deleted if its contents start with
|
|
@@ -3416,12 +3514,17 @@ async function removeEmptyParents(startDir, stopDir) {
|
|
|
3416
3514
|
//#endregion
|
|
3417
3515
|
//#region src/engine/orchestrator.ts
|
|
3418
3516
|
async function generate(spec, emitter, options) {
|
|
3419
|
-
const
|
|
3420
|
-
const
|
|
3517
|
+
const scopedServices = options.services && options.services.length > 0 ? resolveScopedServices(spec, options.services, options.mountRules) : void 0;
|
|
3518
|
+
const scoped = scopedServices !== void 0;
|
|
3519
|
+
const noPrune = options.noPrune === true || scoped;
|
|
3520
|
+
const skipManifestRead = options.noPrune === true && !scoped;
|
|
3521
|
+
const outputPrevManifestForCtx = skipManifestRead ? null : await readManifest(options.outputDir);
|
|
3522
|
+
const targetManifestForCtx = options.target && !skipManifestRead ? await readManifest(options.target) : null;
|
|
3421
3523
|
const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : outputPrevManifestForCtx ? new Set(outputPrevManifestForCtx.files) : void 0;
|
|
3422
3524
|
const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
|
|
3423
3525
|
...options,
|
|
3424
|
-
priorTargetManifestPaths
|
|
3526
|
+
priorTargetManifestPaths,
|
|
3527
|
+
scopedServices
|
|
3425
3528
|
});
|
|
3426
3529
|
if (options.dryRun) {
|
|
3427
3530
|
if (options.target) {
|
|
@@ -3445,8 +3548,9 @@ async function generate(spec, emitter, options) {
|
|
|
3445
3548
|
currentPaths: outputEmittedPaths,
|
|
3446
3549
|
language: emitter.language,
|
|
3447
3550
|
header,
|
|
3448
|
-
noPrune
|
|
3449
|
-
operations
|
|
3551
|
+
noPrune,
|
|
3552
|
+
operations,
|
|
3553
|
+
scoped
|
|
3450
3554
|
});
|
|
3451
3555
|
const allOutputFiles = [...writeResult.written, ...writeResult.merged];
|
|
3452
3556
|
if (allOutputFiles.length > 0) await formatTargetFiles(emitter, options.outputDir, allOutputFiles);
|
|
@@ -3468,8 +3572,9 @@ async function generate(spec, emitter, options) {
|
|
|
3468
3572
|
currentPaths: targetResult.emittedPaths,
|
|
3469
3573
|
language: emitter.language,
|
|
3470
3574
|
header,
|
|
3471
|
-
noPrune
|
|
3472
|
-
operations
|
|
3575
|
+
noPrune,
|
|
3576
|
+
operations,
|
|
3577
|
+
scoped
|
|
3473
3578
|
});
|
|
3474
3579
|
const allTargetFiles = [
|
|
3475
3580
|
...targetResult.written,
|
|
@@ -3485,11 +3590,15 @@ async function generate(spec, emitter, options) {
|
|
|
3485
3590
|
* then write the fresh manifest. Safe on first adoption (no previous manifest).
|
|
3486
3591
|
*/
|
|
3487
3592
|
async function applyManifestPrune(opts) {
|
|
3488
|
-
const
|
|
3489
|
-
language: opts.language,
|
|
3593
|
+
const records = opts.scoped ? mergeScopedManifestRecords(opts.prevManifest, opts.currentPaths, opts.operations) : {
|
|
3490
3594
|
files: opts.currentPaths,
|
|
3491
3595
|
operations: opts.operations
|
|
3492
3596
|
};
|
|
3597
|
+
const manifestOpts = {
|
|
3598
|
+
language: opts.language,
|
|
3599
|
+
files: records.files,
|
|
3600
|
+
operations: records.operations
|
|
3601
|
+
};
|
|
3493
3602
|
if (opts.noPrune) {
|
|
3494
3603
|
await writeManifest(opts.dir, manifestOpts);
|
|
3495
3604
|
return;
|
|
@@ -3960,4 +4069,4 @@ function summarize(changes) {
|
|
|
3960
4069
|
//#endregion
|
|
3961
4070
|
export { MANIFEST_FILENAME as a, treeShakeFiles as c, collectReferencedNames as d, generateAllFiles as f, resolveOperations as h, generate as i, applyFileHeaders as l, deriveMethodName as m, getEmitter as n, integrateGeneratedFiles as o, generateFiles as p, registerEmitter as r, mapFilesForTargetIntegration as s, diffSpecs as t, buildEmitterContext as u };
|
|
3962
4071
|
|
|
3963
|
-
//# sourceMappingURL=diff-
|
|
4072
|
+
//# sourceMappingURL=diff-B7dMUCoJ.mjs.map
|