@azure-tools/typespec-ts 0.56.1-dev.0 → 0.57.0-dev.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-tools/typespec-ts",
3
- "version": "0.56.1-dev.0",
3
+ "version": "0.57.0-dev.2",
4
4
  "description": "A TypeSpec emitter for TypeScript",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -18,11 +18,11 @@
18
18
  "license": "MIT",
19
19
  "devDependencies": {
20
20
  "@azure-rest/core-client": "^2.7.0",
21
- "@azure-tools/azure-http-specs": "^0.1.0-alpha.44 || >= 0.1.0-alpha.45-dev.1",
21
+ "@azure-tools/azure-http-specs": "^0.1.0-alpha.44 || >= 0.1.0-alpha.45-dev.4",
22
22
  "@azure-tools/typespec-autorest": "^0.71.0 || >= 0.72.0-dev.0",
23
23
  "@azure-tools/typespec-azure-core": "^0.71.0 || >= 0.72.0-dev.0",
24
- "@azure-tools/typespec-azure-resource-manager": "^0.71.0 || >= 0.72.0-dev.0",
25
- "@azure-tools/typespec-client-generator-core": "^0.71.0 || >= 0.72.0-dev.0",
24
+ "@azure-tools/typespec-azure-resource-manager": "^0.71.0 || >= 0.72.0-dev.1",
25
+ "@azure-tools/typespec-client-generator-core": "^0.71.2 || >= 0.72.0-dev.3",
26
26
  "@azure/abort-controller": "^2.1.2",
27
27
  "@azure/core-auth": "^1.6.0",
28
28
  "@azure/core-lro": "^3.1.0",
@@ -39,26 +39,26 @@
39
39
  "@microsoft/api-extractor": "^7.58.1",
40
40
  "@types/pluralize": "^0.0.33",
41
41
  "yaml": "^2.8.3",
42
+ "@typespec/spector": "^0.1.0-alpha.28",
42
43
  "@azure-tools/spector-runner": "^0.1.0",
43
- "@typespec/http-specs": "^0.1.0-alpha.41",
44
44
  "@typespec/spec-api": "^0.1.0-alpha.16",
45
- "@typespec/spector": "^0.1.0-alpha.28",
46
45
  "@typespec/tspd": "^0.77.0",
47
- "@typespec/compiler": "^1.15.0",
48
46
  "@typespec/http": "^1.15.0",
47
+ "@typespec/compiler": "^1.15.0",
49
48
  "@typespec/openapi": "^1.15.0",
50
49
  "@typespec/rest": "^0.85.0",
51
50
  "@typespec/versioning": "^0.85.0",
52
- "@typespec/xml": "^0.85.0"
51
+ "@typespec/xml": "^0.85.0",
52
+ "@typespec/http-specs": "^0.1.0-alpha.41"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@azure-tools/typespec-azure-core": "^0.71.0 || >= 0.72.0-dev.0",
56
- "@azure-tools/typespec-client-generator-core": "^0.71.0 || >= 0.72.0-dev.0",
57
- "@typespec/compiler": "^1.15.0",
58
- "@typespec/rest": "^0.85.0",
56
+ "@azure-tools/typespec-client-generator-core": "^0.71.2 || >= 0.72.0-dev.3",
59
57
  "@typespec/http": "^1.15.0",
58
+ "@typespec/rest": "^0.85.0",
59
+ "@typespec/xml": "^0.85.0",
60
60
  "@typespec/versioning": "^0.85.0",
61
- "@typespec/xml": "^0.85.0"
61
+ "@typespec/compiler": "^1.15.0"
62
62
  },
63
63
  "dependencies": {
64
64
  "fast-xml-parser": "^5.10.1",
@@ -13,6 +13,7 @@ import { provideContext, useContext } from "../../context-manager.js";
13
13
  import { reportDiagnostic } from "../../lib.js";
14
14
  import { visitPackageTypes } from "../../modular/emit-models.js";
15
15
  import { getAllAncestors, getAllProperties } from "../../modular/helpers/operation-helpers.js";
16
+ import { applyVisibilityModelSplit } from "../../modular/helpers/visibility-helpers.js";
16
17
  import { normalizeModelPropertyName } from "../../modular/type-expressions/get-type-expression.js";
17
18
  import type { SdkContext } from "../../utils/interfaces.js";
18
19
  import { NameType, normalizeName } from "../../utils/name-utils.js";
@@ -92,6 +93,7 @@ export function provideSdkTypes(context: SdkContext) {
92
93
  types: new Map<Type, SdkType>(),
93
94
  flattenProperties: new Map<SdkModelPropertyType, SdkFlattenPropertyContext>(),
94
95
  };
96
+ applyVisibilityModelSplit(context);
95
97
  visitPackageTypes(context);
96
98
  enrichFlattenProperties(context, sdkTypesContext.flattenProperties, flattenPropertyModelMap);
97
99
  for (const sdkModel of emitQueue) {
package/src/interfaces.ts CHANGED
@@ -260,6 +260,10 @@ export interface ClientOptions {
260
260
  * Defaults to false.
261
261
  */
262
262
  generateReactNativeTarget?: boolean;
263
+ /**
264
+ * Generates visibility-specific request models. Experimental and disabled by default.
265
+ */
266
+ experimentalSplitModelsByVisibility?: boolean;
263
267
  }
264
268
 
265
269
  export interface ServiceInfo {
package/src/lib.ts CHANGED
@@ -91,6 +91,7 @@ export interface EmitterOptions {
91
91
  * package.json exports). Defaults to `false`.
92
92
  */
93
93
  "generate-react-native-target"?: boolean;
94
+ "experimental-split-models-by-visibility"?: boolean;
94
95
  }
95
96
 
96
97
  export const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
@@ -312,6 +313,12 @@ export const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
312
313
  description:
313
314
  "When set to true, generates React Native build targets (tsconfig, warp target, package.json exports). Defaults to `false`.",
314
315
  },
316
+ "experimental-split-models-by-visibility": {
317
+ type: "boolean",
318
+ nullable: true,
319
+ description:
320
+ "Whether to project request-body models to their write visibility (e.g. WidgetCreate/WidgetCreateOrUpdate) so read-only properties do not leak into input types. Defaults to false",
321
+ },
315
322
  },
316
323
  required: [],
317
324
  };
@@ -0,0 +1,471 @@
1
+ import type {
2
+ SdkHttpOperation,
3
+ SdkModelPropertyType,
4
+ SdkModelType,
5
+ SdkServiceMethod,
6
+ SdkType,
7
+ } from "@azure-tools/typespec-client-generator-core";
8
+ import { UsageFlags } from "@azure-tools/typespec-client-generator-core";
9
+ import {
10
+ Visibility,
11
+ getVisibilitySuffix,
12
+ isVisible,
13
+ resolveRequestVisibility,
14
+ } from "@typespec/http";
15
+
16
+ import { getAllOperationsFromClient } from "../../framework/hooks/sdk-types.js";
17
+ import type { SdkContext } from "../../utils/interfaces.js";
18
+ import { getDirectSubtypes } from "./type-helpers.js";
19
+
20
+ const responseUsageFlags =
21
+ UsageFlags.Output |
22
+ UsageFlags.Exception |
23
+ UsageFlags.LroInitial |
24
+ UsageFlags.LroPolling |
25
+ UsageFlags.LroFinalEnvelope;
26
+
27
+ interface SplitState {
28
+ context: SdkContext;
29
+ /**
30
+ * Every reachable `(model, visibility)` pair in the write-body graph, keyed by
31
+ * an emitter-local model identity plus visibility.
32
+ */
33
+ nodes: Map<string, ModelNode>;
34
+ /**
35
+ * The write clone materialized for each node that `needsClone`, keyed the same
36
+ * way as `nodes`.
37
+ */
38
+ cloneByKey: Map<string, SdkModelType>;
39
+ /** Stable emitter-local IDs preserving `SdkModelType` object identity. */
40
+ modelIds: WeakMap<SdkModelType, number>;
41
+ nextModelId: number;
42
+ }
43
+
44
+ /** A `(model, visibility)` vertex in the write-body reference graph. */
45
+ interface ModelNode {
46
+ model: SdkModelType;
47
+ visibility: Visibility;
48
+ /**
49
+ * Keys of referenced nodes. Discriminated hierarchy links are bidirectional
50
+ * so the whole hierarchy is projected together.
51
+ */
52
+ refKeys: Set<string>;
53
+ /** True if the model's own properties differ between `visibility` and Read. */
54
+ ownShapeDiffers: boolean;
55
+ /** True if this node can reach any shape-differing node. */
56
+ needsClone: boolean;
57
+ }
58
+
59
+ /**
60
+ * Projects request models whose write graph differs from their read graph.
61
+ * This must run before `visitPackageTypes` builds the emit queue.
62
+ */
63
+ export function applyVisibilityModelSplit(context: SdkContext): void {
64
+ if (!context.emitterOptions?.experimentalSplitModelsByVisibility) {
65
+ return;
66
+ }
67
+
68
+ const state = createSplitState(context);
69
+ const methods = context.sdkPackage.clients.flatMap((client) =>
70
+ getAllOperationsFromClient(client),
71
+ );
72
+
73
+ // Phase 1 — collect the reachable `(model, visibility)` reference graph.
74
+ for (const method of methods) {
75
+ collectMethodRoots(state, method);
76
+ }
77
+
78
+ // Phase 2 — mark models whose write graph differs from their read graph.
79
+ markNodesNeedingClones(state);
80
+
81
+ // Phase 3 — materialize projected models and wire their references.
82
+ buildClones(state);
83
+
84
+ // Phase 4 — project the original response models to the canonical Read view.
85
+ projectResponseModelsToRead(state);
86
+
87
+ // Phase 5 — repoint operation parameters and payloads to projected models.
88
+ for (const method of methods) {
89
+ repointMethodBody(state, method);
90
+ }
91
+
92
+ // Register projected models for the subsequent emit queue traversal.
93
+ for (const clone of state.cloneByKey.values()) {
94
+ context.sdkPackage.models.push(clone);
95
+ }
96
+ }
97
+
98
+ /** Creates the graph state using Read as the canonical model visibility. */
99
+ function createSplitState(context: SdkContext): SplitState {
100
+ return {
101
+ context,
102
+ nodes: new Map<string, ModelNode>(),
103
+ cloneByKey: new Map<string, SdkModelType>(),
104
+ modelIds: new WeakMap<SdkModelType, number>(),
105
+ nextModelId: 0,
106
+ };
107
+ }
108
+
109
+ /** Stable key identifying a `(model, visibility)` node (and its clone). */
110
+ function nodeKeyFor(state: SplitState, model: SdkModelType, visibility: Visibility): string {
111
+ let modelId = state.modelIds.get(model);
112
+ if (modelId === undefined) {
113
+ modelId = state.nextModelId++;
114
+ state.modelIds.set(model, modelId);
115
+ }
116
+ return `${modelId}|${visibility}`;
117
+ }
118
+
119
+ /**
120
+ * Collects the write-body roots of a single operation. The root of each
121
+ * `methodParameterSegments` entry is the client-method parameter.
122
+ */
123
+ function collectMethodRoots(state: SplitState, method: SdkServiceMethod<SdkHttpOperation>): void {
124
+ const operation = method.operation;
125
+ const bodyParam = operation?.bodyParam;
126
+ if (!bodyParam) {
127
+ return;
128
+ }
129
+
130
+ const visibility = resolveRequestVisibility(
131
+ state.context.program,
132
+ operation.__raw.operation,
133
+ operation.verb,
134
+ );
135
+
136
+ for (const segment of bodyParam.methodParameterSegments) {
137
+ const methodParam = segment[0];
138
+ if (!methodParam) {
139
+ continue;
140
+ }
141
+ collectReferencedModels(state, methodParam.type, visibility, new Set<string>());
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Cycle-safe DFS that records a `(model, visibility)` node and its outgoing
147
+ * references. Nodes are registered before recursion so cyclic graphs terminate.
148
+ */
149
+ function collectNode(state: SplitState, model: SdkModelType, visibility: Visibility): string {
150
+ const key = nodeKeyFor(state, model, visibility);
151
+ if (state.nodes.has(key)) {
152
+ return key;
153
+ }
154
+
155
+ const node: ModelNode = {
156
+ model,
157
+ visibility,
158
+ refKeys: new Set<string>(),
159
+ ownShapeDiffers: false,
160
+ needsClone: false,
161
+ };
162
+ // Seed before recursing so cycles terminate at the already-seeded node.
163
+ state.nodes.set(key, node);
164
+
165
+ for (const property of model.properties) {
166
+ if (property.kind === "property" && property.__raw) {
167
+ const visibleInWrite = isVisible(state.context.program, property.__raw, visibility);
168
+ const visibleInRead = isVisible(state.context.program, property.__raw, Visibility.Read);
169
+ if (visibleInWrite !== visibleInRead) {
170
+ node.ownShapeDiffers = true;
171
+ }
172
+ if (!visibleInWrite) {
173
+ continue;
174
+ }
175
+ }
176
+
177
+ if (property.kind === "property") {
178
+ collectReferencedModels(state, property.type, visibility, node.refKeys);
179
+ }
180
+ }
181
+
182
+ if (model.additionalProperties) {
183
+ collectReferencedModels(state, model.additionalProperties, visibility, node.refKeys);
184
+ }
185
+
186
+ if (model.baseModel) {
187
+ node.refKeys.add(collectNode(state, model.baseModel, visibility));
188
+ }
189
+
190
+ // Link base and subtypes bidirectionally so the projected hierarchy stays together.
191
+ if (findDiscriminatedRoot(model)) {
192
+ for (const neighbor of discriminatedNeighbors(model)) {
193
+ const neighborKey = collectNode(state, neighbor, visibility);
194
+ node.refKeys.add(neighborKey);
195
+ state.nodes.get(neighborKey)!.refKeys.add(key);
196
+ }
197
+ }
198
+
199
+ return key;
200
+ }
201
+
202
+ /** Collects model references nested in supported container types. */
203
+ function collectReferencedModels(
204
+ state: SplitState,
205
+ type: SdkType,
206
+ visibility: Visibility,
207
+ refKeys: Set<string>,
208
+ ): void {
209
+ if (type.kind === "model") {
210
+ refKeys.add(collectNode(state, type, visibility));
211
+ return;
212
+ }
213
+ switch (type.kind) {
214
+ case "array":
215
+ case "dict":
216
+ collectReferencedModels(state, type.valueType, visibility, refKeys);
217
+ break;
218
+ case "nullable":
219
+ collectReferencedModels(state, type.type, visibility, refKeys);
220
+ break;
221
+ case "tuple":
222
+ for (const valueType of type.valueTypes) {
223
+ collectReferencedModels(state, valueType, visibility, refKeys);
224
+ }
225
+ break;
226
+ case "union":
227
+ for (const variantType of type.variantTypes) {
228
+ collectReferencedModels(state, variantType, visibility, refKeys);
229
+ }
230
+ break;
231
+ }
232
+ }
233
+
234
+ /** The base model and direct subtypes that share a node's discriminated tree. */
235
+ function discriminatedNeighbors(model: SdkModelType): SdkModelType[] {
236
+ const neighbors: SdkModelType[] = [];
237
+ if (model.baseModel) {
238
+ neighbors.push(model.baseModel);
239
+ }
240
+ neighbors.push(...getDirectSubtypes(model));
241
+ return neighbors;
242
+ }
243
+
244
+ /**
245
+ * Walks up the inheritance chain and returns the top-most ancestor that
246
+ * declares a discriminator.
247
+ */
248
+ function findDiscriminatedRoot(model: SdkModelType): SdkModelType | undefined {
249
+ let root: SdkModelType | undefined;
250
+ let current: SdkModelType | undefined = model;
251
+ while (current) {
252
+ if (current.discriminatedSubtypes) {
253
+ root = current;
254
+ }
255
+ current = current.baseModel;
256
+ }
257
+ return root;
258
+ }
259
+
260
+ /**
261
+ * Marks nodes whose write graph differs from the read graph by
262
+ * reverse-propagating from nodes whose own property membership differs.
263
+ */
264
+ function markNodesNeedingClones(state: SplitState): void {
265
+ // Reverse edges propagate a nested difference back to every parent.
266
+ const predecessors = new Map<string, string[]>();
267
+ for (const [key, node] of state.nodes) {
268
+ for (const referencedKey of node.refKeys) {
269
+ let referrers = predecessors.get(referencedKey);
270
+ if (!referrers) {
271
+ referrers = [];
272
+ predecessors.set(referencedKey, referrers);
273
+ }
274
+ referrers.push(key);
275
+ }
276
+ }
277
+
278
+ const queue: string[] = [];
279
+ for (const [key, node] of state.nodes) {
280
+ if (node.ownShapeDiffers) {
281
+ node.needsClone = true;
282
+ queue.push(key);
283
+ }
284
+ }
285
+ while (queue.length > 0) {
286
+ const key = queue.shift()!;
287
+ for (const predecessorKey of predecessors.get(key) ?? []) {
288
+ const predecessor = state.nodes.get(predecessorKey)!;
289
+ if (!predecessor.needsClone) {
290
+ predecessor.needsClone = true;
291
+ queue.push(predecessorKey);
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Creates projected model shells, then wires properties and inheritance after
299
+ * every shell exists. This ordering lets forward, cyclic, and mutually recursive
300
+ * references resolve to projected models instead of the original read models.
301
+ */
302
+ function buildClones(state: SplitState): void {
303
+ // Pass 1: create every shell before wiring references.
304
+ for (const [key, node] of state.nodes) {
305
+ if (!node.needsClone) {
306
+ continue;
307
+ }
308
+ const clone: SdkModelType = {
309
+ ...node.model,
310
+ name: `${node.model.name}${getVisibilitySuffix(node.visibility, Visibility.Read)}`,
311
+ properties: [],
312
+ // Write projections are request-only: preserve serialization flags, force Input,
313
+ // and remove inherited response roles.
314
+ usage: (node.model.usage | UsageFlags.Input) & ~responseUsageFlags,
315
+ };
316
+ state.cloneByKey.set(key, clone);
317
+ }
318
+
319
+ // Pass 2: remove dropped properties and wire surviving references.
320
+ for (const [key, node] of state.nodes) {
321
+ const clone = state.cloneByKey.get(key);
322
+ if (!clone) {
323
+ continue;
324
+ }
325
+ const { model, visibility } = node;
326
+
327
+ const properties: SdkModelPropertyType[] = [];
328
+ for (const property of model.properties) {
329
+ if (
330
+ property.kind === "property" &&
331
+ property.__raw &&
332
+ !isVisible(state.context.program, property.__raw, visibility)
333
+ ) {
334
+ continue;
335
+ }
336
+ let wiredProperty = property;
337
+ if (property.kind === "property") {
338
+ const projectedType = projectTypeReferences(state, property.type, visibility);
339
+ if (projectedType !== property.type) {
340
+ wiredProperty = { ...property, type: projectedType };
341
+ }
342
+ }
343
+ properties.push(wiredProperty);
344
+ }
345
+ clone.properties = properties;
346
+
347
+ if (model.additionalProperties) {
348
+ clone.additionalProperties = projectTypeReferences(
349
+ state,
350
+ model.additionalProperties,
351
+ visibility,
352
+ );
353
+ }
354
+
355
+ if (model.baseModel) {
356
+ clone.baseModel =
357
+ state.cloneByKey.get(nodeKeyFor(state, model.baseModel, visibility)) ?? model.baseModel;
358
+ }
359
+
360
+ if (findDiscriminatedRoot(model)) {
361
+ if (model.discriminatedSubtypes) {
362
+ clone.discriminatedSubtypes = Object.fromEntries(
363
+ Object.entries(model.discriminatedSubtypes).map(([discriminatorValue, subtype]) => [
364
+ discriminatorValue,
365
+ state.cloneByKey.get(nodeKeyFor(state, subtype, visibility)) ?? subtype,
366
+ ]),
367
+ );
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ /** Projects response-used models to the canonical Read visibility. */
374
+ function projectResponseModelsToRead(state: SplitState): void {
375
+ for (const model of state.context.sdkPackage.models) {
376
+ if ((model.usage & responseUsageFlags) === 0) {
377
+ continue;
378
+ }
379
+ model.properties = model.properties.filter(
380
+ (property) =>
381
+ property.kind !== "property" ||
382
+ !property.__raw ||
383
+ isVisible(state.context.program, property.__raw, Visibility.Read),
384
+ );
385
+ }
386
+ }
387
+
388
+ /** Replaces model references nested in supported container types. */
389
+ function projectTypeReferences(state: SplitState, type: SdkType, visibility: Visibility): SdkType {
390
+ if (type.kind === "model") {
391
+ return state.cloneByKey.get(nodeKeyFor(state, type, visibility)) ?? type;
392
+ }
393
+ switch (type.kind) {
394
+ case "array":
395
+ case "dict": {
396
+ const projectedValueType = projectTypeReferences(state, type.valueType, visibility);
397
+ return projectedValueType === type.valueType
398
+ ? type
399
+ : { ...type, valueType: projectedValueType };
400
+ }
401
+ case "nullable": {
402
+ const projectedType = projectTypeReferences(state, type.type, visibility);
403
+ return projectedType === type.type ? type : { ...type, type: projectedType };
404
+ }
405
+ case "tuple": {
406
+ const projectedValueTypes = type.valueTypes.map((valueType) =>
407
+ projectTypeReferences(state, valueType, visibility),
408
+ );
409
+ return projectedValueTypes.every((valueType, index) => valueType === type.valueTypes[index])
410
+ ? type
411
+ : { ...type, valueTypes: projectedValueTypes };
412
+ }
413
+ case "union": {
414
+ const projectedVariantTypes = type.variantTypes.map((variantType) =>
415
+ projectTypeReferences(state, variantType, visibility),
416
+ );
417
+ return projectedVariantTypes.every(
418
+ (variantType, index) => variantType === type.variantTypes[index],
419
+ )
420
+ ? type
421
+ : { ...type, variantTypes: projectedVariantTypes };
422
+ }
423
+ }
424
+ return type;
425
+ }
426
+
427
+ /**
428
+ * Repoints client-method parameters and their HTTP payload representation to
429
+ * projected models.
430
+ */
431
+ function repointMethodBody(state: SplitState, method: SdkServiceMethod<SdkHttpOperation>): void {
432
+ const operation = method.operation;
433
+ const bodyParam = operation?.bodyParam;
434
+ if (!bodyParam) {
435
+ return;
436
+ }
437
+
438
+ const visibility = resolveRequestVisibility(
439
+ state.context.program,
440
+ operation.__raw.operation,
441
+ operation.verb,
442
+ );
443
+
444
+ for (const segment of bodyParam.methodParameterSegments) {
445
+ const methodParam = segment[0];
446
+ if (!methodParam) {
447
+ continue;
448
+ }
449
+
450
+ const originalType = methodParam.type;
451
+ const projectedType = projectTypeReferences(state, originalType, visibility);
452
+ if (projectedType === originalType) {
453
+ // The write view is identical to the read model (collapse).
454
+ continue;
455
+ }
456
+
457
+ methodParam.type = projectedType;
458
+
459
+ if (originalType === bodyParam.type) {
460
+ // A non-spread body uses the method parameter model as the payload model.
461
+ bodyParam.type = projectedType;
462
+ } else if (bodyParam.type.kind === "model") {
463
+ // A spread body uses a synthesized wrapper serialized property by property.
464
+ for (const bodyProperty of bodyParam.type.properties) {
465
+ if (bodyProperty.type === originalType) {
466
+ bodyProperty.type = projectedType;
467
+ }
468
+ }
469
+ }
470
+ }
471
+ }
@@ -57,6 +57,8 @@ function extractClientOptions(
57
57
  const ignoreNullableOnOptional = getIgnoreNullableOnOptional(emitterOptions);
58
58
  const wrapNonModelReturn = getWrapNonModelReturn(emitterOptions);
59
59
  const isMultiService = (dpgContext.allServiceNamespaces?.length ?? 0) > 1;
60
+ const experimentalSplitModelsByVisibility =
61
+ emitterOptions["experimental-split-models-by-visibility"] === true;
60
62
 
61
63
  return {
62
64
  ...credentialInfo,
@@ -85,6 +87,7 @@ function extractClientOptions(
85
87
  enableStorageCompat,
86
88
  treatUnknownAsRecord,
87
89
  generateReactNativeTarget,
90
+ experimentalSplitModelsByVisibility,
88
91
  };
89
92
  }
90
93