@kubb/plugin-faker 5.0.0-beta.4 → 5.0.0-beta.56

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.
Files changed (38) hide show
  1. package/README.md +39 -22
  2. package/dist/{Faker-CdyPfOPg.d.ts → Faker-A5UuxwJj.d.ts} +3 -3
  3. package/dist/{Faker-fcQEB9i5.js → Faker-CHh0JtBG.js} +41 -145
  4. package/dist/Faker-CHh0JtBG.js.map +1 -0
  5. package/dist/{Faker-BgleOzVN.cjs → Faker-CcGjn5ZM.cjs} +40 -174
  6. package/dist/Faker-CcGjn5ZM.cjs.map +1 -0
  7. package/dist/components.cjs +1 -1
  8. package/dist/components.d.ts +1 -1
  9. package/dist/components.js +1 -1
  10. package/dist/{fakerGenerator-D7daHCh6.js → fakerGenerator-DDNsdbH2.js} +237 -94
  11. package/dist/fakerGenerator-DDNsdbH2.js.map +1 -0
  12. package/dist/{fakerGenerator-VJEVzLjc.cjs → fakerGenerator-DrwGWYwv.cjs} +240 -97
  13. package/dist/fakerGenerator-DrwGWYwv.cjs.map +1 -0
  14. package/dist/fakerGenerator-KKVr-CA2.d.ts +14 -0
  15. package/dist/generators.cjs +1 -1
  16. package/dist/generators.d.ts +1 -1
  17. package/dist/generators.js +1 -1
  18. package/dist/index.cjs +240 -69
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +35 -15
  21. package/dist/index.js +241 -70
  22. package/dist/index.js.map +1 -1
  23. package/dist/{printerFaker-CJiwzoto.d.ts → printerFaker-CMCJT3FB.d.ts} +68 -35
  24. package/package.json +12 -22
  25. package/src/components/Faker.tsx +51 -65
  26. package/src/generators/fakerGenerator.tsx +108 -72
  27. package/src/plugin.ts +27 -23
  28. package/src/printers/printerFaker.ts +102 -40
  29. package/src/resolvers/resolverFaker.ts +31 -39
  30. package/src/types.ts +40 -31
  31. package/src/utils.ts +7 -106
  32. package/dist/Faker-BgleOzVN.cjs.map +0 -1
  33. package/dist/Faker-fcQEB9i5.js.map +0 -1
  34. package/dist/fakerGenerator-C3Ho3BaI.d.ts +0 -9
  35. package/dist/fakerGenerator-D7daHCh6.js.map +0 -1
  36. package/dist/fakerGenerator-VJEVzLjc.cjs.map +0 -1
  37. package/extension.yaml +0 -364
  38. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
@@ -1,21 +1,19 @@
1
+ import { getPerContentTypeName, resolveContentTypeVariants } from '@internals/shared'
2
+ import { aliasConflictingImports, filterUsedImports, rewriteAliasedImports } from '@internals/utils'
1
3
  import { ast, defineGenerator } from '@kubb/core'
2
4
  import { pluginTsName } from '@kubb/plugin-ts'
3
5
  import { File, jsxRenderer } from '@kubb/renderer-jsx'
4
6
  import { Faker } from '../components/Faker.tsx'
5
7
  import { printerFaker } from '../printers/printerFaker.ts'
6
8
  import type { PluginFaker } from '../types.ts'
7
- import {
8
- aliasConflictingImports,
9
- buildResponseUnionSchema,
10
- canOverrideSchema,
11
- filterUsedImports,
12
- localeToFakerImport,
13
- resolveParamNameByLocation,
14
- resolveSchemaRef,
15
- resolveTypeReference,
16
- rewriteAliasedImports,
17
- } from '../utils.ts'
9
+ import { buildResponseUnionSchema, canOverrideSchema, localeToFakerImport, resolveParamNameByLocation, resolveTypeReference } from '../utils.ts'
18
10
 
11
+ /**
12
+ * Built-in generator for `@kubb/plugin-faker`. Emits one `createX` factory
13
+ * per schema in the spec plus per-operation request/response factories. Each
14
+ * factory returns a value matching the corresponding TypeScript type from
15
+ * `@kubb/plugin-ts`.
16
+ */
19
17
  export const fakerGenerator = defineGenerator<PluginFaker>({
20
18
  name: 'faker',
21
19
  renderer: jsxRenderer,
@@ -24,26 +22,29 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
24
22
  const { output, group, dateParser, regexGenerator, mapper, seed, locale, printer } = ctx.options
25
23
  const pluginTs = ctx.driver.getPlugin(pluginTsName)
26
24
 
27
- if (!node.name || !pluginTs || !adapter.inputNode) {
25
+ if (!node.name || !pluginTs) {
28
26
  return
29
27
  }
30
28
 
31
29
  const tsResolver = ctx.driver.getResolver(pluginTsName)
32
30
 
33
- const schemaNode = resolveSchemaRef(node, adapter.inputNode.schemas)
34
- const schemaName = schemaNode.name ?? node.name
35
- const mode = ctx.getMode(output)
31
+ const schemaName = node.name
32
+ const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum)
33
+ const tsEnumType = pluginTs.options?.enum?.type
34
+ const tsEnumTypeSuffix = pluginTs.options?.enum?.typeSuffix ?? 'Key'
35
+ const schemaTypeName =
36
+ isEnumSchema && tsEnumType === 'asConst' ? tsResolver.resolveEnumKeyName({ name: schemaName }, tsEnumTypeSuffix) : tsResolver.resolveTypeName(schemaName)
36
37
  const meta = {
37
38
  name: resolver.resolveName(schemaName),
38
- file: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }),
39
- typeName: tsResolver.resolveTypeName(schemaName),
39
+ file: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }),
40
+ typeName: schemaTypeName,
40
41
  typeFile: tsResolver.resolveFile(
41
42
  { name: schemaName, extname: '.ts' },
42
- { root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group },
43
+ { root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group ?? undefined },
43
44
  ),
44
45
  } as const
45
- const canOverride = canOverrideSchema(schemaNode)
46
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
46
+ const canOverride = canOverrideSchema(node)
47
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
47
48
  const printerInstance = printerFaker({
48
49
  resolver,
49
50
  schemaName,
@@ -54,9 +55,9 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
54
55
  nodes: printer?.nodes,
55
56
  cyclicSchemas,
56
57
  })
57
- const fakerText = printerInstance.print(schemaNode) ?? 'undefined'
58
+ const fakerText = printerInstance.print(node) ?? 'undefined'
58
59
  const typeReference = resolveTypeReference({
59
- node: schemaNode,
60
+ node,
60
61
  canOverride,
61
62
  name: meta.name,
62
63
  typeName: meta.typeName,
@@ -64,12 +65,10 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
64
65
  typeFilePath: meta.typeFile.path,
65
66
  })
66
67
 
67
- const imports = adapter
68
- .getImports(schemaNode, (schemaName) => ({
69
- name: resolver.resolveName(schemaName),
70
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
71
- }))
72
- .filter((entry) => entry.path !== meta.file.path)
68
+ const imports = adapter.getImports(node, (schemaName) => ({
69
+ name: resolver.resolveName(schemaName),
70
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
71
+ }))
73
72
  const usedImports = filterUsedImports(imports, fakerText)
74
73
 
75
74
  return (
@@ -77,20 +76,21 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
77
76
  baseName={meta.file.baseName}
78
77
  path={meta.file.path}
79
78
  meta={meta.file.meta}
80
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
81
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
79
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
80
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
82
81
  >
83
82
  <File.Import name={locale ? [{ propertyName: localeToFakerImport(locale), name: 'faker' }] : ['faker']} path="@faker-js/faker" />
84
83
  {regexGenerator === 'randexp' && <File.Import name={'RandExp'} path={'randexp'} />}
85
84
  {dateParser !== 'faker' && <File.Import path={dateParser} name={dateParser} />}
86
85
  {typeReference.importPath && <File.Import isTypeOnly root={meta.file.path} path={typeReference.importPath} name={[meta.typeName]} />}
87
- {mode === 'split' &&
88
- usedImports.map((imp) => <File.Import key={[schemaName, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />)}
86
+ {usedImports.map((imp) => (
87
+ <File.Import key={[schemaName, imp.path, imp.name].join('-')} root={meta.file.path} path={imp.path} name={imp.name} />
88
+ ))}
89
89
  <Faker
90
90
  name={meta.name}
91
91
  typeName={typeReference.typeName}
92
- description={schemaNode.description}
93
- node={schemaNode}
92
+ description={node.description}
93
+ node={node}
94
94
  printer={printerInstance}
95
95
  seed={seed}
96
96
  canOverride={canOverride}
@@ -115,31 +115,66 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
115
115
  name: resolveParamNameByLocation(resolver, node, param),
116
116
  typeName: resolveParamNameByLocation(tsResolver, node, param),
117
117
  }))
118
- const responseEntries = node.responses.map((response) => ({
119
- response,
120
- name: resolver.resolveResponseStatusName(node, response.statusCode),
121
- typeName: tsResolver.resolveResponseStatusName(node, response.statusCode),
122
- }))
123
- const dataEntry = node.requestBody?.content?.[0]?.schema
124
- ? {
125
- schema: {
126
- ...node.requestBody.content![0]!.schema!,
127
- description: node.requestBody.description ?? node.requestBody.content![0]!.schema!.description,
128
- },
129
- name: resolver.resolveDataName(node),
130
- typeName: tsResolver.resolveDataName(node),
131
- description: node.requestBody.description ?? node.requestBody.content![0]!.schema!.description,
132
- }
133
- : null
118
+ type RenderUnit = { schema: ast.SchemaNode | null; name: string; typeName: string; description?: string; skipImportNames: Array<string> }
119
+
120
+ // Expands a content array into render units: one faker per content type plus a union faker
121
+ // (named `baseName`) when more than one content type carries a schema, else a single faker.
122
+ function expandContentUnits(
123
+ entries: Array<{ contentType: string; schema?: ast.SchemaNode | null }>,
124
+ baseName: string,
125
+ tsBaseName: string,
126
+ description: string | undefined,
127
+ decorate?: (schema: ast.SchemaNode) => ast.SchemaNode,
128
+ ): Array<RenderUnit> {
129
+ const withSchema = entries.filter((entry) => entry.schema)
130
+ if (withSchema.length <= 1) {
131
+ const primary = withSchema[0] ?? entries[0]
132
+ if (!primary?.schema) return []
133
+ return [{ schema: decorate ? decorate(primary.schema) : primary.schema, name: baseName, typeName: tsBaseName, description, skipImportNames: [] }]
134
+ }
135
+ const variants = resolveContentTypeVariants(entries, baseName)
136
+ const unionSchema = ast.createSchema({ type: 'union', members: variants.map((variant) => ast.createSchema({ type: 'ref', name: variant.name })) })
137
+ return [
138
+ ...variants.map((variant) => ({
139
+ schema: decorate ? decorate(variant.schema) : variant.schema,
140
+ name: variant.name,
141
+ typeName: getPerContentTypeName(tsBaseName, variant.suffix),
142
+ description,
143
+ skipImportNames: [],
144
+ })),
145
+ { schema: unionSchema, name: baseName, typeName: tsBaseName, description, skipImportNames: variants.map((variant) => variant.name) },
146
+ ]
147
+ }
148
+
149
+ const responseUnits = node.responses.flatMap((response) =>
150
+ expandContentUnits(
151
+ response.content ?? [],
152
+ resolver.resolveResponseStatusName(node, response.statusCode),
153
+ tsResolver.resolveResponseStatusName(node, response.statusCode),
154
+ response.description,
155
+ ),
156
+ )
157
+ const dataUnits = expandContentUnits(
158
+ node.requestBody?.content ?? [],
159
+ resolver.resolveDataName(node),
160
+ tsResolver.resolveDataName(node),
161
+ node.requestBody?.description,
162
+ (schema) => ({ ...schema, description: node.requestBody?.description ?? schema.description }),
163
+ )
134
164
  const responseName = resolver.resolveResponseName(node)
135
165
  const localHelperNames = new Set([
136
166
  ...paramEntries.map((entry) => entry.name),
137
- ...responseEntries.map((entry) => entry.name),
138
- ...(dataEntry ? [dataEntry.name] : []),
167
+ ...responseUnits.map((unit) => unit.name),
168
+ ...dataUnits.map((unit) => unit.name),
139
169
  responseName,
140
170
  ])
171
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
172
+
141
173
  const meta = {
142
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
174
+ file: resolver.resolveFile(
175
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
176
+ { root, output, group: group ?? undefined },
177
+ ),
143
178
  typeFile: tsResolver.resolveFile(
144
179
  {
145
180
  name: node.operationId,
@@ -150,7 +185,7 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
150
185
  {
151
186
  root,
152
187
  output: pluginTs.options?.output ?? output,
153
- group: pluginTs.options?.group,
188
+ group: pluginTs.options?.group ?? undefined,
154
189
  },
155
190
  ),
156
191
  } as const
@@ -159,7 +194,7 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
159
194
  return adapter
160
195
  .getImports(schema, (schemaName) => ({
161
196
  name: resolver.resolveName(schemaName),
162
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
197
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
163
198
  }))
164
199
  .filter((entry) => entry.path !== meta.file.path)
165
200
  }
@@ -182,7 +217,6 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
182
217
  }
183
218
 
184
219
  const canOverride = canOverrideSchema(schema)
185
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
186
220
  const printerInstance = printerFaker({
187
221
  resolver,
188
222
  schemaName: name,
@@ -230,8 +264,8 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
230
264
  baseName={meta.file.baseName}
231
265
  path={meta.file.path}
232
266
  meta={meta.file.meta}
233
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
234
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
267
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
268
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
235
269
  >
236
270
  <File.Import name={locale ? [{ propertyName: localeToFakerImport(locale), name: 'faker' }] : ['faker']} path="@faker-js/faker" />
237
271
  {regexGenerator === 'randexp' && <File.Import name={'RandExp'} path={'randexp'} />}
@@ -243,27 +277,29 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
243
277
  typeName,
244
278
  }),
245
279
  )}
246
- {responseEntries.map(({ response, name, typeName }) =>
280
+ {responseUnits.map((unit) =>
247
281
  renderEntry({
248
- schema: response.schema,
249
- name,
250
- typeName,
251
- description: response.description,
282
+ schema: unit.schema,
283
+ name: unit.name,
284
+ typeName: unit.typeName,
285
+ description: unit.description,
286
+ skipImportNames: unit.skipImportNames,
287
+ }),
288
+ )}
289
+ {dataUnits.map((unit) =>
290
+ renderEntry({
291
+ schema: unit.schema,
292
+ name: unit.name,
293
+ typeName: unit.typeName,
294
+ description: unit.description,
295
+ skipImportNames: unit.skipImportNames,
252
296
  }),
253
297
  )}
254
- {dataEntry
255
- ? renderEntry({
256
- schema: dataEntry.schema,
257
- name: dataEntry.name,
258
- typeName: dataEntry.typeName,
259
- description: dataEntry.description,
260
- })
261
- : null}
262
298
  {renderEntry({
263
299
  schema: buildResponseUnionSchema(node, resolver),
264
300
  name: responseName,
265
301
  typeName: tsResolver.resolveResponseName(node),
266
- skipImportNames: responseEntries.map(({ name }) => name),
302
+ skipImportNames: responseUnits.map((unit) => unit.name),
267
303
  })}
268
304
  </File>
269
305
  )
package/src/plugin.ts CHANGED
@@ -1,28 +1,45 @@
1
- import { camelCase } from '@internals/utils'
2
- import { definePlugin, type Group } from '@kubb/core'
1
+ import { createGroupConfig } from '@internals/shared'
2
+ import { definePlugin } from '@kubb/core'
3
3
  import { pluginTsName } from '@kubb/plugin-ts'
4
4
  import { fakerGenerator } from './generators/fakerGenerator.tsx'
5
5
  import { resolverFaker } from './resolvers/resolverFaker.ts'
6
6
  import type { PluginFaker } from './types.ts'
7
7
 
8
8
  /**
9
- * Canonical plugin name for `@kubb/plugin-faker`, used in driver lookups and warnings.
9
+ * Canonical plugin name for `@kubb/plugin-faker`. Used for driver lookups and
10
+ * cross-plugin dependency references.
10
11
  */
11
12
  export const pluginFakerName = 'plugin-faker' satisfies PluginFaker['name']
12
13
 
13
14
  /**
14
- * Generates Faker mock data factories from OpenAPI/AST specification.
15
- *
16
- * Creates randomized test data and mock helpers from schema definitions.
15
+ * Generates one mock-data factory per OpenAPI schema using Faker.js. Call
16
+ * `createPet()` to get a realistic `Pet` object. Useful for tests, Storybook,
17
+ * and local development without a running backend.
17
18
  *
18
19
  * @example
19
- * `import pluginFaker from '@kubb/plugin-faker'; export default defineConfig({ plugins: [pluginFaker({ output: { path: 'mocks' } })], })`
20
+ * ```ts
21
+ * import { defineConfig } from 'kubb'
22
+ * import { pluginTs } from '@kubb/plugin-ts'
23
+ * import { pluginFaker } from '@kubb/plugin-faker'
24
+ *
25
+ * export default defineConfig({
26
+ * input: { path: './petStore.yaml' },
27
+ * output: { path: './src/gen' },
28
+ * plugins: [
29
+ * pluginTs(),
30
+ * pluginFaker({
31
+ * output: { path: './mocks' },
32
+ * seed: [100],
33
+ * }),
34
+ * ],
35
+ * })
36
+ * ```
20
37
  */
21
38
  export const pluginFaker = definePlugin<PluginFaker>((options) => {
22
39
  const {
23
- output = { path: 'mocks', barrelType: 'named' },
40
+ output = { path: 'mocks', barrel: { type: 'named' } },
24
41
  seed,
25
- locale,
42
+ locale = 'en',
26
43
  group,
27
44
  exclude = [],
28
45
  include,
@@ -37,20 +54,7 @@ export const pluginFaker = definePlugin<PluginFaker>((options) => {
37
54
  transformer: userTransformer,
38
55
  } = options
39
56
 
40
- const groupConfig = group
41
- ? ({
42
- ...group,
43
- name: group.name
44
- ? group.name
45
- : (ctx: { group: string }) => {
46
- if (group.type === 'path') {
47
- return `${ctx.group.split('/')[1]}`
48
- }
49
-
50
- return `${camelCase(ctx.group)}Controller`
51
- },
52
- } satisfies Group)
53
- : undefined
57
+ const groupConfig = createGroupConfig(group)
54
58
 
55
59
  return {
56
60
  name: pluginFakerName,
@@ -1,14 +1,32 @@
1
- import { stringify, toRegExpString } from '@internals/utils'
1
+ import { buildObject, extractRefName, objectKey, stringify, toRegExpString } from '@kubb/ast/utils'
2
2
  import { ast } from '@kubb/core'
3
3
  import type { PluginFaker, ResolverFaker } from '../types.ts'
4
4
 
5
5
  /**
6
- * Partial printer nodes for Faker generation, mapping schema types to output strings.
6
+ * Partial map of node-type overrides for the Faker printer. Each key is a
7
+ * `SchemaType` (`'string'`, `'date'`, ...) and each handler returns the
8
+ * Faker expression for that schema as a string. Use `this.transform` to
9
+ * recurse into nested schema nodes and `this.options` to read printer options.
10
+ *
11
+ * @example Override the integer handler
12
+ * ```ts
13
+ * pluginFaker({
14
+ * printer: {
15
+ * nodes: {
16
+ * integer() {
17
+ * return 'faker.number.float()'
18
+ * },
19
+ * },
20
+ * },
21
+ * })
22
+ * ```
7
23
  */
8
24
  export type PrinterFakerNodes = ast.PrinterPartial<string, PrinterFakerOptions>
9
25
 
10
26
  /**
11
- * Configuration options for the Faker printer, including resolvers, mappers, and cyclic schema tracking.
27
+ * Options passed to the Faker printer at instantiation: the parser library
28
+ * for date strings, the regex generator, the user-supplied schema-name
29
+ * mapper, and the resolver used to compute identifiers.
12
30
  */
13
31
  export type PrinterFakerOptions = {
14
32
  dateParser?: PluginFaker['resolvedOptions']['dateParser']
@@ -18,6 +36,12 @@ export type PrinterFakerOptions = {
18
36
  typeName?: string
19
37
  schemaName?: string
20
38
  nestedInObject?: boolean
39
+ /**
40
+ * Set while printing the members of a union (`oneOf`). Object properties then index their
41
+ * type as `(NonNullable<T> & Record<K, unknown>)[K]` instead of `NonNullable<T>[K]`, so a key
42
+ * carried by only some branches stays valid (a plain index would be a TS2339).
43
+ */
44
+ nestedInUnion?: boolean
21
45
  nodes?: PrinterFakerNodes
22
46
  /**
23
47
  * Names of schemas that participate in a circular dependency chain.
@@ -85,7 +109,7 @@ const fakerKeywordMapper = {
85
109
  },
86
110
  boolean: () => 'faker.datatype.boolean()',
87
111
  null: () => 'null',
88
- array: (items: string[] = [], min?: number, max?: number) => {
112
+ array: (items: Array<string> = [], min?: number, max?: number) => {
89
113
  if (items.length > 1) {
90
114
  return `faker.helpers.arrayElements([${items.join(', ')}])`
91
115
  }
@@ -106,9 +130,9 @@ const fakerKeywordMapper = {
106
130
 
107
131
  return `faker.helpers.multiple(() => (${item}))`
108
132
  },
109
- tuple: (items: string[] = []) => `[${items.join(', ')}]`,
133
+ tuple: (items: Array<string> = []) => `[${items.join(', ')}]`,
110
134
  enum: (items: Array<string | number | boolean | undefined> = [], type = 'any') => `faker.helpers.arrayElement<${type}>([${items.join(', ')}])`,
111
- union: (items: string[] = []) => `faker.helpers.arrayElement<any>([${items.join(', ')}])`,
135
+ union: (items: Array<string> = []) => `faker.helpers.arrayElement<any>([${items.join(', ')}])`,
112
136
  datetime: () => 'faker.date.anytime().toISOString()',
113
137
  date: (representation: 'date' | 'string' = 'string', parser: PluginFaker['resolvedOptions']['dateParser'] = 'faker') => {
114
138
  if (representation === 'string') {
@@ -142,7 +166,7 @@ const fakerKeywordMapper = {
142
166
  },
143
167
  uuid: () => 'faker.string.uuid()',
144
168
  url: () => 'faker.internet.url()',
145
- and: (items: string[] = []) => {
169
+ and: (items: Array<string> = []) => {
146
170
  if (items.length === 0) {
147
171
  return '{}'
148
172
  }
@@ -180,6 +204,32 @@ function parseEnumValue(value: string | number | boolean | undefined) {
180
204
  return value
181
205
  }
182
206
 
207
+ /** Reads the discriminator literal off a variant, or `undefined` when it can't be determined. */
208
+ function getDiscriminatorValue(member: ast.SchemaNode, discriminatorPropertyName: string) {
209
+ const prop = ast.narrowSchema(member, 'object')?.properties?.find((p) => p.name === discriminatorPropertyName)
210
+ const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
211
+
212
+ return enumNode ? getEnumValues(enumNode)[0] : undefined
213
+ }
214
+
215
+ /**
216
+ * Type expression for an object property's value, indexed off the parent `typeName`.
217
+ *
218
+ * In a union (`oneOf`), a key that only some branches declare turns a plain `NonNullable<T>[K]`
219
+ * into a TS2339 error, so union members guard the access. The breakdown is below.
220
+ */
221
+ function indexedTypeName(typeName: string, propertyName: string, nestedInUnion?: boolean): string {
222
+ const key = JSON.stringify(propertyName)
223
+
224
+ // `(NonNullable<T> & Record<K, unknown>)[K]`, read inside-out:
225
+ // NonNullable<T> strips null and undefined from the parent type T.
226
+ // & Record<K, unknown> forces every branch to have key K. A branch that already declares K
227
+ // keeps it (`T[K] & unknown` is `T[K]`); a branch missing K gains it as `unknown`.
228
+ // [K] reads the key, which is now always present, so it never hits TS2339.
229
+ // For a single object T the intersection does nothing, leaving `T[K]`.
230
+ return nestedInUnion ? `(NonNullable<${typeName}> & Record<${key}, unknown>)[${key}]` : `NonNullable<${typeName}>[${key}]`
231
+ }
232
+
183
233
  /**
184
234
  * Creates a Faker printer that generates mock data generation code from schema nodes.
185
235
  * Handles circular references gracefully by emitting memoizing getters for cyclic properties.
@@ -234,7 +284,7 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
234
284
  // Use the canonical name from the $ref path — node.name may have been overridden
235
285
  // (e.g. by single-member allOf flatten using the property-derived child name).
236
286
  // Inline refs (without $ref) from faker utils already carry resolved helper names.
237
- const refName = node.ref ? (ast.extractRefName(node.ref) ?? node.name ?? node.schema?.name) : (node.name ?? node.schema?.name)
287
+ const refName = node.ref ? (extractRefName(node.ref) ?? node.name ?? node.schema?.name) : (node.name ?? node.schema?.name)
238
288
 
239
289
  if (!refName) {
240
290
  throw new Error('Name not defined for ref node')
@@ -258,18 +308,32 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
258
308
  return fakerKeywordMapper.enum(getEnumValues(node).map(parseEnumValue), this.options.typeName)
259
309
  },
260
310
  union(node): string {
261
- const items: string[] = (node.members ?? [])
262
- .map((member) =>
263
- printNested(member, {
264
- nestedInObject: true,
265
- }),
266
- )
311
+ const { discriminatorPropertyName } = node
312
+ const baseTypeName = this.options.typeName
313
+
314
+ const items: Array<string> = (node.members ?? [])
315
+ .map((member) => {
316
+ // For a discriminated union, narrow each variant to its own branch so nested
317
+ // `NonNullable<T>[K]` indexes resolve against that branch instead of the whole union.
318
+ const value = discriminatorPropertyName ? getDiscriminatorValue(member, discriminatorPropertyName) : undefined
319
+
320
+ if (baseTypeName && value !== undefined) {
321
+ const typeName = `Extract<NonNullable<${baseTypeName}>, { ${JSON.stringify(discriminatorPropertyName)}: ${parseEnumValue(value)} }>`
322
+
323
+ return printNested(member, { typeName, nestedInObject: true })
324
+ }
325
+
326
+ // Without a discriminator, keep the union type but guard each indexed access (see
327
+ // `indexedTypeName`) so a key carried by only some branches resolves to `unknown`
328
+ // rather than erroring with TS2339.
329
+ return printNested(member, { typeName: baseTypeName, nestedInObject: true, nestedInUnion: true })
330
+ })
267
331
  .filter((item): item is string => Boolean(item))
268
332
 
269
333
  return fakerKeywordMapper.union(items)
270
334
  },
271
335
  intersection(node): string {
272
- const items: string[] = (node.members ?? [])
336
+ const items: Array<string> = (node.members ?? [])
273
337
  .map((member) =>
274
338
  printNested(member, {
275
339
  nestedInObject: true,
@@ -280,7 +344,7 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
280
344
  return fakerKeywordMapper.and(items)
281
345
  },
282
346
  array(node): string {
283
- const items: string[] = (node.items ?? [])
347
+ const items: Array<string> = (node.items ?? [])
284
348
  .map((member) =>
285
349
  printNested(member, {
286
350
  typeName: this.options.typeName ? `NonNullable<${this.options.typeName}>[number]` : undefined,
@@ -292,7 +356,7 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
292
356
  return fakerKeywordMapper.array(items, node.min, node.max)
293
357
  },
294
358
  tuple(node): string {
295
- const items: string[] = (node.items ?? [])
359
+ const items: Array<string> = (node.items ?? [])
296
360
  .map((member, index) =>
297
361
  printNested(member, {
298
362
  typeName: this.options.typeName ? `NonNullable<${this.options.typeName}>[${index}]` : undefined,
@@ -305,32 +369,30 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
305
369
  },
306
370
  object(node): string {
307
371
  const cyclicSchemas = this.options.cyclicSchemas
308
- const properties = (node.properties ?? [])
309
- .map((property): string => {
310
- if (this.options.mapper && Object.hasOwn(this.options.mapper, property.name)) {
311
- return `"${property.name}": ${this.options.mapper[property.name]}`
312
- }
372
+ const entries = (node.properties ?? []).map((property): string => {
373
+ if (this.options.mapper && Object.hasOwn(this.options.mapper, property.name)) {
374
+ return `${objectKey(property.name)}: ${this.options.mapper[property.name]}`
375
+ }
376
+
377
+ const value: string =
378
+ printNested(property.schema, {
379
+ typeName: this.options.typeName ? indexedTypeName(this.options.typeName, property.name, this.options.nestedInUnion) : undefined,
380
+ nestedInObject: true,
381
+ }) ?? 'undefined'
313
382
 
314
- const value: string =
315
- printNested(property.schema, {
316
- typeName: this.options.typeName ? `NonNullable<${this.options.typeName}>[${JSON.stringify(property.name)}]` : undefined,
317
- nestedInObject: true,
318
- }) ?? 'undefined'
319
-
320
- // When the property's schema transitively references a schema that is
321
- // part of a circular dependency (other than the current schema itself),
322
- // emit a memoizing lazy getter. On first access it computes the value,
323
- // replaces itself with a plain data property via Object.defineProperty,
324
- // and returns the cached value – so every subsequent read is stable.
325
- if (cyclicSchemas && ast.containsCircularRef(property.schema, { circularSchemas: cyclicSchemas, excludeName: this.options.schemaName })) {
326
- return `get ${property.name}() { const _value = ${value}; Object.defineProperty(this, ${JSON.stringify(property.name)}, { value: _value, configurable: true, writable: true, enumerable: true }); return _value }`
327
- }
383
+ // When the property's schema transitively references a schema that is
384
+ // part of a circular dependency (other than the current schema itself),
385
+ // emit a memoizing lazy getter. On first access it computes the value,
386
+ // replaces itself with a plain data property via Object.defineProperty,
387
+ // and returns the cached value – so every subsequent read is stable.
388
+ if (cyclicSchemas && ast.containsCircularRef(property.schema, { circularSchemas: cyclicSchemas, excludeName: this.options.schemaName })) {
389
+ return `get ${objectKey(property.name)}() { const _value = ${value}; Object.defineProperty(this, ${JSON.stringify(property.name)}, { value: _value, configurable: true, writable: true, enumerable: true }); return _value }`
390
+ }
328
391
 
329
- return `"${property.name}": ${value}`
330
- })
331
- .join(',')
392
+ return `${objectKey(property.name)}: ${value}`
393
+ })
332
394
 
333
- return `{${properties}}`
395
+ return buildObject(entries)
334
396
  },
335
397
  ...options.nodes,
336
398
  },