@kubb/plugin-faker 5.0.0-beta.3 → 5.0.0-beta.30

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 (37) hide show
  1. package/README.md +26 -5
  2. package/dist/{Faker-CdyPfOPg.d.ts → Faker-BaLJxPyl.d.ts} +2 -2
  3. package/dist/{Faker-fcQEB9i5.js → Faker-CXZVQQ7e.js} +36 -99
  4. package/dist/Faker-CXZVQQ7e.js.map +1 -0
  5. package/dist/{Faker-BgleOzVN.cjs → Faker-CkJccVKI.cjs} +35 -122
  6. package/dist/Faker-CkJccVKI.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-BvMBDgwp.js} +126 -32
  11. package/dist/fakerGenerator-BvMBDgwp.js.map +1 -0
  12. package/dist/fakerGenerator-DSvAJTq3.d.ts +15 -0
  13. package/dist/{fakerGenerator-VJEVzLjc.cjs → fakerGenerator-DhNV9xBw.cjs} +127 -33
  14. package/dist/fakerGenerator-DhNV9xBw.cjs.map +1 -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 +177 -36
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +33 -12
  21. package/dist/index.js +178 -37
  22. package/dist/index.js.map +1 -1
  23. package/dist/{printerFaker-CJiwzoto.d.ts → printerFaker-Bhwq62d1.d.ts} +63 -26
  24. package/extension.yaml +817 -0
  25. package/package.json +8 -13
  26. package/src/components/Faker.tsx +44 -63
  27. package/src/generators/fakerGenerator.tsx +35 -35
  28. package/src/plugin.ts +23 -6
  29. package/src/printers/printerFaker.ts +80 -16
  30. package/src/resolvers/resolverFaker.ts +29 -37
  31. package/src/types.ts +36 -23
  32. package/src/utils.ts +6 -105
  33. package/dist/Faker-BgleOzVN.cjs.map +0 -1
  34. package/dist/Faker-fcQEB9i5.js.map +0 -1
  35. package/dist/fakerGenerator-C3Ho3BaI.d.ts +0 -9
  36. package/dist/fakerGenerator-D7daHCh6.js.map +0 -1
  37. package/dist/fakerGenerator-VJEVzLjc.cjs.map +0 -1
package/package.json CHANGED
@@ -1,12 +1,10 @@
1
1
  {
2
2
  "name": "@kubb/plugin-faker",
3
- "version": "5.0.0-beta.3",
4
- "description": "Faker.js data generator plugin for Kubb, creating realistic mock data from OpenAPI specifications for development and testing.",
3
+ "version": "5.0.0-beta.30",
4
+ "description": "Generate Faker.js mock data factories from your OpenAPI schemas. Produces realistic seed data, test fixtures, and datasets for development and testing.",
5
5
  "keywords": [
6
- "code-generator",
6
+ "code-generation",
7
7
  "codegen",
8
- "data-generation",
9
- "development",
10
8
  "faker",
11
9
  "faker.js",
12
10
  "fakerjs",
@@ -14,11 +12,8 @@
14
12
  "kubb",
15
13
  "mock-data",
16
14
  "mocks",
17
- "oas",
18
15
  "openapi",
19
- "plugins",
20
16
  "swagger",
21
- "testing",
22
17
  "typescript"
23
18
  ],
24
19
  "license": "MIT",
@@ -31,7 +26,7 @@
31
26
  "files": [
32
27
  "src",
33
28
  "dist",
34
- "plugin.json",
29
+ "extension.yaml",
35
30
  "!/**/**.test.**",
36
31
  "!/**/__tests__/**",
37
32
  "!/**/__snapshots__/**"
@@ -71,15 +66,15 @@
71
66
  "registry": "https://registry.npmjs.org/"
72
67
  },
73
68
  "dependencies": {
74
- "@kubb/core": "5.0.0-beta.3",
75
- "@kubb/renderer-jsx": "5.0.0-beta.3",
76
- "@kubb/plugin-ts": "5.0.0-beta.3"
69
+ "@kubb/core": "5.0.0-beta.29",
70
+ "@kubb/renderer-jsx": "5.0.0-beta.29",
71
+ "@kubb/plugin-ts": "5.0.0-beta.30"
77
72
  },
78
73
  "devDependencies": {
79
74
  "@internals/utils": "0.0.0"
80
75
  },
81
76
  "peerDependencies": {
82
- "@kubb/renderer-jsx": "5.0.0-beta.3"
77
+ "@kubb/renderer-jsx": "5.0.0-beta.29"
83
78
  },
84
79
  "size-limit": [
85
80
  {
@@ -44,71 +44,17 @@ export function Faker({ node, description, name, typeName, printer, seed, canOve
44
44
  const isTuple = node.type === 'tuple'
45
45
  const isScalar = SCALAR_TYPES.has(node.type)
46
46
 
47
- let fakerTextWithOverride = fakerText
48
- let useGenericOverride = false
49
-
50
- if (canOverride && isObject) {
51
- useGenericOverride = true
52
- }
53
-
54
- if (canOverride && isTuple) {
55
- fakerTextWithOverride = `data || ${fakerText}`
56
- }
57
-
58
- if (canOverride && isArray) {
59
- fakerTextWithOverride = `[
60
- ...${fakerText},
61
- ...(data || [])
62
- ]`
63
- }
64
-
65
- if (canOverride && isScalar) {
66
- fakerTextWithOverride = `data ?? ${fakerText}`
67
- }
47
+ const useGenericOverride = canOverride && isObject
48
+ const fakerTextWithOverride = (() => {
49
+ if (canOverride && isTuple) return `data || ${fakerText}`
50
+ if (canOverride && isArray) return `[\n ...${fakerText},\n ...(data || [])\n]`
51
+ if (canOverride && isScalar) return `data ?? ${fakerText}`
52
+ return fakerText
53
+ })()
68
54
 
69
55
  const { dataType, returnType: resolvedReturnType } = resolveFakerTypeUsage(node, typeName, canOverride)
70
56
 
71
- let functionSignature = ''
72
- let functionBody = ''
73
-
74
- if (useGenericOverride) {
75
- // Generate function with defaultFakeData structure
76
- const jsdoc = description ? `/**\n * @description ${jsStringEscape(description)}\n */\n ` : ''
77
- functionSignature = `${jsdoc}export function ${name}(data?: Partial<${typeName}>): Required<${typeName}>`
78
-
79
- const seedCode = seed ? `faker.seed(${JSON.stringify(seed)})\n ` : ''
80
-
81
- // When the object node has properties that transitively reference a cyclic schema,
82
- // the printer emits memoizing getters for those properties. Spreading the object
83
- // literal would immediately invoke those getters, triggering recursive faker calls
84
- // and causing a stack overflow. Detect this upfront via ast helpers so we can
85
- // use Object.defineProperty-based merging instead of spread.
86
- const { cyclicSchemas, schemaName } = printer.options
87
- const hasGetters =
88
- node.type === 'object' &&
89
- !!cyclicSchemas &&
90
- (node.properties ?? []).some((p) => ast.containsCircularRef(p.schema, { circularSchemas: cyclicSchemas, excludeName: schemaName }))
91
-
92
- if (hasGetters) {
93
- functionBody = `{
94
- ${seedCode}const defaultFakeData = ${fakerText}
95
- if (data) {
96
- for (const [key, value] of Object.entries(data)) {
97
- Object.defineProperty(defaultFakeData, key, { value, configurable: true, writable: true, enumerable: true })
98
- }
99
- }
100
- return defaultFakeData as Required<${typeName}>
101
- }`
102
- } else {
103
- functionBody = `{
104
- ${seedCode}const defaultFakeData = ${fakerText}
105
- return {
106
- ...defaultFakeData,
107
- ...(data || {}),
108
- } as Required<${typeName}>
109
- }`
110
- }
111
- } else {
57
+ if (!useGenericOverride) {
112
58
  const usesData = /\bdata\b/.test(fakerTextWithOverride)
113
59
  const dataParamName = usesData ? 'data' : '_data'
114
60
  const params = ast.createFunctionParameters({
@@ -130,7 +76,7 @@ export function Faker({ node, description, name, typeName, printer, seed, canOve
130
76
  name={name}
131
77
  JSDoc={{ comments: description ? [`@description ${jsStringEscape(description)}`] : [] }}
132
78
  params={canOverride ? paramsSignature : undefined}
133
- returnType={returnType}
79
+ returnType={returnType ?? undefined}
134
80
  >
135
81
  {seed ? (
136
82
  <>
@@ -144,6 +90,41 @@ export function Faker({ node, description, name, typeName, printer, seed, canOve
144
90
  )
145
91
  }
146
92
 
93
+ // Generate function with defaultFakeData structure
94
+ const jsdoc = description ? `/**\n * @description ${jsStringEscape(description)}\n */\n ` : ''
95
+ const functionSignature = `${jsdoc}export function ${name}(data?: Partial<${typeName}>): Required<${typeName}>`
96
+
97
+ const seedCode = seed ? `faker.seed(${JSON.stringify(seed)})\n ` : ''
98
+
99
+ // When the object node has properties that transitively reference a cyclic schema,
100
+ // the printer emits memoizing getters for those properties. Spreading the object
101
+ // literal would immediately invoke those getters, triggering recursive faker calls
102
+ // and causing a stack overflow. Detect this upfront via ast helpers so we can
103
+ // use Object.defineProperty-based merging instead of spread.
104
+ const { cyclicSchemas, schemaName } = printer.options
105
+ const hasGetters =
106
+ node.type === 'object' &&
107
+ !!cyclicSchemas &&
108
+ (node.properties ?? []).some((p) => ast.containsCircularRef(p.schema, { circularSchemas: cyclicSchemas, excludeName: schemaName }))
109
+
110
+ const functionBody = hasGetters
111
+ ? `{
112
+ ${seedCode}const defaultFakeData = ${fakerText}
113
+ if (data) {
114
+ for (const [key, value] of Object.entries(data)) {
115
+ Object.defineProperty(defaultFakeData, key, { value, configurable: true, writable: true, enumerable: true })
116
+ }
117
+ }
118
+ return defaultFakeData as Required<${typeName}>
119
+ }`
120
+ : `{
121
+ ${seedCode}const defaultFakeData = ${fakerText}
122
+ return {
123
+ ...defaultFakeData,
124
+ ...(data || {}),
125
+ } as Required<${typeName}>
126
+ }`
127
+
147
128
  return (
148
129
  <File.Source name={name} isExportable isIndexable>
149
130
  {functionSignature}
@@ -1,49 +1,45 @@
1
+ import { aliasConflictingImports, filterUsedImports, rewriteAliasedImports } from '@internals/utils'
1
2
  import { ast, defineGenerator } from '@kubb/core'
2
3
  import { pluginTsName } from '@kubb/plugin-ts'
3
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
4
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
4
5
  import { Faker } from '../components/Faker.tsx'
5
6
  import { printerFaker } from '../printers/printerFaker.ts'
6
7
  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'
8
+ import { buildResponseUnionSchema, canOverrideSchema, localeToFakerImport, resolveParamNameByLocation, resolveTypeReference } from '../utils.ts'
18
9
 
10
+ /**
11
+ * Built-in generator for `@kubb/plugin-faker`. Emits one `createX` factory
12
+ * per schema in the spec plus per-operation request/response factories. Each
13
+ * factory returns a value matching the corresponding TypeScript type from
14
+ * `@kubb/plugin-ts`.
15
+ */
19
16
  export const fakerGenerator = defineGenerator<PluginFaker>({
20
17
  name: 'faker',
21
- renderer: jsxRenderer,
18
+ renderer: jsxRendererSync,
22
19
  schema(node, ctx) {
23
20
  const { adapter, config, resolver, root } = ctx
24
21
  const { output, group, dateParser, regexGenerator, mapper, seed, locale, printer } = ctx.options
25
22
  const pluginTs = ctx.driver.getPlugin(pluginTsName)
26
23
 
27
- if (!node.name || !pluginTs || !adapter.inputNode) {
24
+ if (!node.name || !pluginTs) {
28
25
  return
29
26
  }
30
27
 
31
28
  const tsResolver = ctx.driver.getResolver(pluginTsName)
32
29
 
33
- const schemaNode = resolveSchemaRef(node, adapter.inputNode.schemas)
34
- const schemaName = schemaNode.name ?? node.name
30
+ const schemaName = node.name
35
31
  const mode = ctx.getMode(output)
36
32
  const meta = {
37
33
  name: resolver.resolveName(schemaName),
38
- file: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }),
34
+ file: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }),
39
35
  typeName: tsResolver.resolveTypeName(schemaName),
40
36
  typeFile: tsResolver.resolveFile(
41
37
  { name: schemaName, extname: '.ts' },
42
- { root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group },
38
+ { root, output: pluginTs.options?.output ?? output, group: pluginTs.options?.group ?? undefined },
43
39
  ),
44
40
  } as const
45
- const canOverride = canOverrideSchema(schemaNode)
46
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
41
+ const canOverride = canOverrideSchema(node)
42
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
47
43
  const printerInstance = printerFaker({
48
44
  resolver,
49
45
  schemaName,
@@ -54,9 +50,9 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
54
50
  nodes: printer?.nodes,
55
51
  cyclicSchemas,
56
52
  })
57
- const fakerText = printerInstance.print(schemaNode) ?? 'undefined'
53
+ const fakerText = printerInstance.print(node) ?? 'undefined'
58
54
  const typeReference = resolveTypeReference({
59
- node: schemaNode,
55
+ node,
60
56
  canOverride,
61
57
  name: meta.name,
62
58
  typeName: meta.typeName,
@@ -65,9 +61,9 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
65
61
  })
66
62
 
67
63
  const imports = adapter
68
- .getImports(schemaNode, (schemaName) => ({
64
+ .getImports(node, (schemaName) => ({
69
65
  name: resolver.resolveName(schemaName),
70
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
66
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
71
67
  }))
72
68
  .filter((entry) => entry.path !== meta.file.path)
73
69
  const usedImports = filterUsedImports(imports, fakerText)
@@ -77,8 +73,8 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
77
73
  baseName={meta.file.baseName}
78
74
  path={meta.file.path}
79
75
  meta={meta.file.meta}
80
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
81
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
76
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
77
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
82
78
  >
83
79
  <File.Import name={locale ? [{ propertyName: localeToFakerImport(locale), name: 'faker' }] : ['faker']} path="@faker-js/faker" />
84
80
  {regexGenerator === 'randexp' && <File.Import name={'RandExp'} path={'randexp'} />}
@@ -89,8 +85,8 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
89
85
  <Faker
90
86
  name={meta.name}
91
87
  typeName={typeReference.typeName}
92
- description={schemaNode.description}
93
- node={schemaNode}
88
+ description={node.description}
89
+ node={node}
94
90
  printer={printerInstance}
95
91
  seed={seed}
96
92
  canOverride={canOverride}
@@ -138,8 +134,13 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
138
134
  ...(dataEntry ? [dataEntry.name] : []),
139
135
  responseName,
140
136
  ])
137
+ const cyclicSchemas = new Set<string>(ctx.meta.circularNames)
138
+
141
139
  const meta = {
142
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
140
+ file: resolver.resolveFile(
141
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
142
+ { root, output, group: group ?? undefined },
143
+ ),
143
144
  typeFile: tsResolver.resolveFile(
144
145
  {
145
146
  name: node.operationId,
@@ -150,7 +151,7 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
150
151
  {
151
152
  root,
152
153
  output: pluginTs.options?.output ?? output,
153
- group: pluginTs.options?.group,
154
+ group: pluginTs.options?.group ?? undefined,
154
155
  },
155
156
  ),
156
157
  } as const
@@ -159,7 +160,7 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
159
160
  return adapter
160
161
  .getImports(schema, (schemaName) => ({
161
162
  name: resolver.resolveName(schemaName),
162
- path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group }).path,
163
+ path: resolver.resolveFile({ name: schemaName, extname: '.ts' }, { root, output, group: group ?? undefined }).path,
163
164
  }))
164
165
  .filter((entry) => entry.path !== meta.file.path)
165
166
  }
@@ -182,7 +183,6 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
182
183
  }
183
184
 
184
185
  const canOverride = canOverrideSchema(schema)
185
- const cyclicSchemas = adapter.inputNode ? ast.findCircularSchemas(adapter.inputNode.schemas) : undefined
186
186
  const printerInstance = printerFaker({
187
187
  resolver,
188
188
  schemaName: name,
@@ -230,8 +230,8 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
230
230
  baseName={meta.file.baseName}
231
231
  path={meta.file.path}
232
232
  meta={meta.file.meta}
233
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
234
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
233
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
234
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })}
235
235
  >
236
236
  <File.Import name={locale ? [{ propertyName: localeToFakerImport(locale), name: 'faker' }] : ['faker']} path="@faker-js/faker" />
237
237
  {regexGenerator === 'randexp' && <File.Import name={'RandExp'} path={'randexp'} />}
@@ -245,7 +245,7 @@ export const fakerGenerator = defineGenerator<PluginFaker>({
245
245
  )}
246
246
  {responseEntries.map(({ response, name, typeName }) =>
247
247
  renderEntry({
248
- schema: response.schema,
248
+ schema: response.content?.[0]?.schema ?? null,
249
249
  name,
250
250
  typeName,
251
251
  description: response.description,
package/src/plugin.ts CHANGED
@@ -6,17 +6,34 @@ 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 {
@@ -50,7 +67,7 @@ export const pluginFaker = definePlugin<PluginFaker>((options) => {
50
67
  return `${camelCase(ctx.group)}Controller`
51
68
  },
52
69
  } satisfies Group)
53
- : undefined
70
+ : null
54
71
 
55
72
  return {
56
73
  name: pluginFakerName,
@@ -3,12 +3,30 @@ 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.
@@ -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,
@@ -313,7 +377,7 @@ export const printerFaker: (options: PrinterFakerOptions) => ast.Printer<Printer
313
377
 
314
378
  const value: string =
315
379
  printNested(property.schema, {
316
- typeName: this.options.typeName ? `NonNullable<${this.options.typeName}>[${JSON.stringify(property.name)}]` : undefined,
380
+ typeName: this.options.typeName ? indexedTypeName(this.options.typeName, property.name, this.options.nestedInUnion) : undefined,
317
381
  nestedInObject: true,
318
382
  }) ?? 'undefined'
319
383