@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.
- package/README.md +26 -5
- package/dist/{Faker-CdyPfOPg.d.ts → Faker-BaLJxPyl.d.ts} +2 -2
- package/dist/{Faker-fcQEB9i5.js → Faker-CXZVQQ7e.js} +36 -99
- package/dist/Faker-CXZVQQ7e.js.map +1 -0
- package/dist/{Faker-BgleOzVN.cjs → Faker-CkJccVKI.cjs} +35 -122
- package/dist/Faker-CkJccVKI.cjs.map +1 -0
- package/dist/components.cjs +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +1 -1
- package/dist/{fakerGenerator-D7daHCh6.js → fakerGenerator-BvMBDgwp.js} +126 -32
- package/dist/fakerGenerator-BvMBDgwp.js.map +1 -0
- package/dist/fakerGenerator-DSvAJTq3.d.ts +15 -0
- package/dist/{fakerGenerator-VJEVzLjc.cjs → fakerGenerator-DhNV9xBw.cjs} +127 -33
- package/dist/fakerGenerator-DhNV9xBw.cjs.map +1 -0
- package/dist/generators.cjs +1 -1
- package/dist/generators.d.ts +1 -1
- package/dist/generators.js +1 -1
- package/dist/index.cjs +177 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +33 -12
- package/dist/index.js +178 -37
- package/dist/index.js.map +1 -1
- package/dist/{printerFaker-CJiwzoto.d.ts → printerFaker-Bhwq62d1.d.ts} +63 -26
- package/extension.yaml +817 -0
- package/package.json +8 -13
- package/src/components/Faker.tsx +44 -63
- package/src/generators/fakerGenerator.tsx +35 -35
- package/src/plugin.ts +23 -6
- package/src/printers/printerFaker.ts +80 -16
- package/src/resolvers/resolverFaker.ts +29 -37
- package/src/types.ts +36 -23
- package/src/utils.ts +6 -105
- package/dist/Faker-BgleOzVN.cjs.map +0 -1
- package/dist/Faker-fcQEB9i5.js.map +0 -1
- package/dist/fakerGenerator-C3Ho3BaI.d.ts +0 -9
- package/dist/fakerGenerator-D7daHCh6.js.map +0 -1
- 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.
|
|
4
|
-
"description": "Faker.js data
|
|
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-
|
|
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
|
-
"
|
|
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.
|
|
75
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
76
|
-
"@kubb/plugin-ts": "5.0.0-beta.
|
|
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.
|
|
77
|
+
"@kubb/renderer-jsx": "5.0.0-beta.29"
|
|
83
78
|
},
|
|
84
79
|
"size-limit": [
|
|
85
80
|
{
|
package/src/components/Faker.tsx
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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
|
|
24
|
+
if (!node.name || !pluginTs) {
|
|
28
25
|
return
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
const tsResolver = ctx.driver.getResolver(pluginTsName)
|
|
32
29
|
|
|
33
|
-
const
|
|
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(
|
|
46
|
-
const cyclicSchemas =
|
|
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(
|
|
53
|
+
const fakerText = printerInstance.print(node) ?? 'undefined'
|
|
58
54
|
const typeReference = resolveTypeReference({
|
|
59
|
-
node
|
|
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(
|
|
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(
|
|
81
|
-
footer={resolver.resolveFooter(
|
|
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={
|
|
93
|
-
node={
|
|
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(
|
|
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(
|
|
234
|
-
footer={resolver.resolveFooter(
|
|
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
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
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
|
-
:
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
380
|
+
typeName: this.options.typeName ? indexedTypeName(this.options.typeName, property.name, this.options.nestedInUnion) : undefined,
|
|
317
381
|
nestedInObject: true,
|
|
318
382
|
}) ?? 'undefined'
|
|
319
383
|
|