@kubb/core 1.0.0-beta.1 → 1.0.0-beta.10

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.
@@ -7,7 +7,7 @@ import { Queue } from '../../utils/queue'
7
7
 
8
8
  import type { QueueTask } from '../../utils/queue'
9
9
  import type { Argument0, Strategy } from './types'
10
- import type { KubbConfig, KubbPlugin, PluginLifecycleHooks, PluginLifecycle, MaybePromise, ResolveIdParams } from '../../types'
10
+ import type { KubbConfig, KubbPlugin, PluginLifecycleHooks, PluginLifecycle, MaybePromise, ResolvePathParams, ResolveNameParams } from '../../types'
11
11
  import type { Logger } from '../../build'
12
12
  import type { CorePluginOptions } from '../../plugin'
13
13
 
@@ -19,7 +19,8 @@ const hookNames: {
19
19
  } = {
20
20
  validate: 1,
21
21
  buildStart: 1,
22
- resolveId: 1,
22
+ resolvePath: 1,
23
+ resolveName: 1,
23
24
  load: 1,
24
25
  transform: 1,
25
26
  writeFile: 1,
@@ -50,70 +51,174 @@ export class PluginManager {
50
51
  config,
51
52
  fileManager: this.fileManager,
52
53
  load: this.load,
53
- resolveId: this.resolveId,
54
+ resolvePath: this.resolvePath,
55
+ resolveName: this.resolveName,
54
56
  }) as KubbPlugin<CorePluginOptions> & {
55
57
  api: CorePluginOptions['api']
56
58
  }
57
59
  this.plugins = [this.core, ...(config.plugins || [])]
58
60
  }
59
61
 
60
- resolveId = (params: ResolveIdParams) => {
62
+ resolvePath = (params: ResolvePathParams) => {
61
63
  if (params.pluginName) {
62
- return this.hookForPlugin(params.pluginName, 'resolveId', [params.fileName, params.directory, params.options])
64
+ return this.hookForPluginSync({
65
+ pluginName: params.pluginName,
66
+ hookName: 'resolvePath',
67
+ parameters: [params.fileName, params.directory, params.options],
68
+ })
69
+ }
70
+ return this.hookFirstSync({
71
+ hookName: 'resolvePath',
72
+ parameters: [params.fileName, params.directory, params.options],
73
+ })
74
+ }
75
+
76
+ resolveName = (params: ResolveNameParams) => {
77
+ if (params.pluginName) {
78
+ return this.hookForPluginSync({
79
+ pluginName: params.pluginName,
80
+ hookName: 'resolveName',
81
+ parameters: [params.name],
82
+ })
63
83
  }
64
- return this.hookFirst('resolveId', [params.fileName, params.directory, params.options])
84
+ return this.hookFirstSync({
85
+ hookName: 'resolveName',
86
+ parameters: [params.name],
87
+ })
65
88
  }
66
89
 
67
90
  load = async (id: string) => {
68
- return this.hookFirst('load', [id])
91
+ return this.hookFirst({
92
+ hookName: 'load',
93
+ parameters: [id],
94
+ })
95
+ }
96
+
97
+ /**
98
+ *
99
+ * Run only hook for a specific plugin name
100
+ */
101
+ hookForPlugin<H extends PluginLifecycleHooks>({
102
+ pluginName,
103
+ hookName,
104
+ parameters,
105
+ }: {
106
+ pluginName: string
107
+ hookName: H
108
+ parameters: Parameters<PluginLifecycle[H]>
109
+ }): Promise<ReturnType<PluginLifecycle[H]> | null> {
110
+ const plugin = this.getPlugin(hookName, pluginName)
111
+
112
+ return this.run({
113
+ strategy: 'hookFirst',
114
+ hookName,
115
+ parameters,
116
+ plugin,
117
+ })
69
118
  }
70
119
 
71
- // run only hook for a specific plugin name
72
- hookForPlugin<H extends PluginLifecycleHooks>(
73
- pluginName: string,
74
- hookName: H,
75
- parameters: Parameters<PluginLifecycle[H]>,
120
+ hookForPluginSync<H extends PluginLifecycleHooks>({
121
+ pluginName,
122
+ hookName,
123
+ parameters,
124
+ }: {
125
+ pluginName: string
126
+ hookName: H
127
+ parameters: Parameters<PluginLifecycle[H]>
128
+ }): ReturnType<PluginLifecycle[H]> | null {
129
+ const plugin = this.getPlugin(hookName, pluginName)
130
+
131
+ return this.runSync({
132
+ strategy: 'hookFirst',
133
+ hookName,
134
+ parameters,
135
+ plugin,
136
+ })
137
+ }
138
+
139
+ /**
140
+ *
141
+ * Chains, first non-null result stops and returns
142
+ */
143
+ hookFirst<H extends PluginLifecycleHooks>({
144
+ hookName,
145
+ parameters,
146
+ skipped,
147
+ }: {
148
+ hookName: H
149
+ parameters: Parameters<PluginLifecycle[H]>
76
150
  skipped?: ReadonlySet<KubbPlugin> | null
77
- ): Promise<ReturnType<PluginLifecycle[H]> | null> {
151
+ }): Promise<ReturnType<PluginLifecycle[H]> | null> {
78
152
  let promise: Promise<ReturnType<PluginLifecycle[H]> | null> = Promise.resolve(null)
79
- for (const plugin of this.getSortedPlugins(hookName, pluginName)) {
153
+ for (const plugin of this.getSortedPlugins(hookName)) {
80
154
  if (skipped && skipped.has(plugin)) continue
81
155
  promise = promise.then((result) => {
82
156
  if (result != null) return result
83
- return this.run('hookFirst', hookName, parameters, plugin) as typeof result
157
+ return this.run({
158
+ strategy: 'hookFirst',
159
+ hookName,
160
+ parameters,
161
+ plugin,
162
+ }) as typeof result
84
163
  })
85
164
  }
86
165
  return promise
87
166
  }
88
167
 
89
- // chains, first non-null result stops and returns
90
- hookFirst<H extends PluginLifecycleHooks>(
91
- hookName: H,
92
- parameters: Parameters<PluginLifecycle[H]>,
168
+ /**
169
+ *
170
+ * Chains, first non-null result stops and returns
171
+ */
172
+ hookFirstSync<H extends PluginLifecycleHooks>({
173
+ hookName,
174
+ parameters,
175
+ skipped,
176
+ }: {
177
+ hookName: H
178
+ parameters: Parameters<PluginLifecycle[H]>
93
179
  skipped?: ReadonlySet<KubbPlugin> | null
94
- ): Promise<ReturnType<PluginLifecycle[H]> | null> {
95
- let promise: Promise<ReturnType<PluginLifecycle[H]> | null> = Promise.resolve(null)
180
+ }): ReturnType<PluginLifecycle[H]> | null {
181
+ let result = null
182
+
96
183
  for (const plugin of this.getSortedPlugins(hookName)) {
97
184
  if (skipped && skipped.has(plugin)) continue
98
- promise = promise.then((result) => {
99
- if (result != null) return result
100
- return this.run('hookFirst', hookName, parameters, plugin) as typeof result
185
+
186
+ result = this.runSync({
187
+ strategy: 'hookFirst',
188
+ hookName,
189
+ parameters,
190
+ plugin,
101
191
  })
192
+
193
+ if (result != null) {
194
+ break
195
+ }
102
196
  }
103
- return promise
197
+ return result
104
198
  }
105
199
 
106
200
  // parallel
107
- async hookParallel<H extends PluginLifecycleHooks, TOuput = void>(hookName: H, parameters?: Parameters<PluginLifecycle[H]> | undefined) {
201
+ async hookParallel<H extends PluginLifecycleHooks, TOuput = void>({
202
+ hookName,
203
+ parameters,
204
+ }: {
205
+ hookName: H
206
+ parameters?: Parameters<PluginLifecycle[H]> | undefined
207
+ }) {
108
208
  const parallelPromises: Promise<TOuput>[] = []
109
209
 
110
210
  for (const plugin of this.getSortedPlugins(hookName)) {
111
211
  if ((plugin[hookName] as { sequential?: boolean })?.sequential) {
112
212
  await Promise.all(parallelPromises)
113
213
  parallelPromises.length = 0
114
- await this.run('hookParallel', hookName, parameters, plugin)
214
+ await this.run({
215
+ strategy: 'hookParallel',
216
+ hookName,
217
+ parameters,
218
+ plugin,
219
+ })
115
220
  } else {
116
- const promise: Promise<TOuput> = this.run('hookParallel', hookName, parameters, plugin)
221
+ const promise: Promise<TOuput> = this.run({ strategy: 'hookParallel', hookName, parameters, plugin })
117
222
 
118
223
  parallelPromises.push(promise)
119
224
  }
@@ -122,17 +227,26 @@ export class PluginManager {
122
227
  }
123
228
 
124
229
  // chains, reduces returned value, handling the reduced value as the first hook argument
125
- hookReduceArg0<H extends PluginLifecycleHooks>(
126
- hookName: H,
127
- [argument0, ...rest]: Parameters<PluginLifecycle[H]>,
230
+ hookReduceArg0<H extends PluginLifecycleHooks>({
231
+ hookName,
232
+ parameters,
233
+ reduce,
234
+ }: {
235
+ hookName: H
236
+ parameters: Parameters<PluginLifecycle[H]>
128
237
  reduce: (reduction: Argument0<H>, result: ReturnType<PluginLifecycle[H]>, plugin: KubbPlugin) => MaybePromise<Argument0<H> | null>
129
- ): Promise<Argument0<H>> {
238
+ }): Promise<Argument0<H>> {
239
+ const [argument0, ...rest] = parameters
240
+
130
241
  let promise: Promise<Argument0<H>> = Promise.resolve(argument0)
131
242
  for (const plugin of this.getSortedPlugins(hookName)) {
132
243
  promise = promise.then((argument0) =>
133
- this.run('hookReduceArg0', hookName, [argument0, ...rest] as Parameters<PluginLifecycle[H]>, plugin).then((result) =>
134
- reduce.call(this.core.api, argument0, result as ReturnType<PluginLifecycle[H]>, plugin)
135
- )
244
+ this.run({
245
+ strategy: 'hookReduceArg0',
246
+ hookName,
247
+ parameters: [argument0, ...rest] as Parameters<PluginLifecycle[H]>,
248
+ plugin,
249
+ }).then((result) => reduce.call(this.core.api, argument0, result as ReturnType<PluginLifecycle[H]>, plugin))
136
250
  ) as Promise<Argument0<H>>
137
251
  }
138
252
  return promise
@@ -140,30 +254,39 @@ export class PluginManager {
140
254
 
141
255
  // chains
142
256
 
143
- hookSeq<H extends PluginLifecycleHooks>(hookName: H, parameters?: Parameters<PluginLifecycle[H]>) {
257
+ hookSeq<H extends PluginLifecycleHooks>({ hookName, parameters }: { hookName: H; parameters?: Parameters<PluginLifecycle[H]> }) {
144
258
  let promise: Promise<void> = Promise.resolve()
145
259
  for (const plugin of this.getSortedPlugins(hookName)) {
146
- promise = promise.then(() => this.run('hookSeq', hookName, parameters, plugin))
260
+ promise = promise.then(() =>
261
+ this.run({
262
+ strategy: 'hookSeq',
263
+ hookName,
264
+ parameters,
265
+ plugin,
266
+ })
267
+ )
147
268
  }
148
269
  return promise.then(noReturn)
149
270
  }
150
271
 
151
- private getSortedPlugins(hookName: keyof PluginLifecycle, pluginName?: string): KubbPlugin[] {
272
+ private getSortedPlugins(_hookName: keyof PluginLifecycle): KubbPlugin[] {
152
273
  const plugins = [...this.plugins]
153
274
 
154
- if (pluginName) {
155
- const pluginsByPluginName = plugins.filter((item) => item.name === pluginName && item[hookName])
156
- if (pluginsByPluginName.length === 0) {
157
- // fallback on the core plugin when there is no match
158
- if (this.config.logLevel === 'warn' && this.logger?.spinner) {
159
- this.logger.spinner.info(`Plugin hook with ${hookName} not found for plugin ${pluginName}`)
160
- }
161
- return [this.core]
275
+ return plugins
276
+ }
277
+
278
+ private getPlugin(hookName: keyof PluginLifecycle, pluginName: string): KubbPlugin {
279
+ const plugins = [...this.plugins]
280
+
281
+ const pluginByPluginName = plugins.find((item) => item.name === pluginName && item[hookName])
282
+ if (!pluginByPluginName) {
283
+ // fallback on the core plugin when there is no match
284
+ if (this.config.logLevel === 'warn' && this.logger?.spinner) {
285
+ this.logger.spinner.info(`Plugin hook with ${hookName} not found for plugin ${pluginName}`)
162
286
  }
163
- return pluginsByPluginName
287
+ return this.core
164
288
  }
165
-
166
- return plugins
289
+ return pluginByPluginName
167
290
  }
168
291
 
169
292
  /**
@@ -173,12 +296,17 @@ export class PluginManager {
173
296
  * @param plugin The actual pluginObject to run.
174
297
  */
175
298
  // Implementation signature
176
- private run<H extends PluginLifecycleHooks, TResult = void>(
177
- strategy: Strategy,
178
- hookName: H,
179
- parameters: unknown[] | undefined,
299
+ private run<H extends PluginLifecycleHooks, TResult = void>({
300
+ strategy,
301
+ hookName,
302
+ parameters,
303
+ plugin,
304
+ }: {
305
+ strategy: Strategy
306
+ hookName: H
307
+ parameters: unknown[] | undefined
180
308
  plugin: KubbPlugin
181
- ): Promise<TResult> {
309
+ }): Promise<TResult> {
182
310
  const hook = plugin[hookName]!
183
311
 
184
312
  return Promise.resolve()
@@ -221,20 +349,31 @@ export class PluginManager {
221
349
  * @param plugin The acutal plugin
222
350
  * @param replaceContext When passed, the plugin context can be overridden.
223
351
  */
224
- private runSync<H extends PluginLifecycleHooks>(
225
- hookName: H,
226
- parameters: Parameters<PluginLifecycle[H]>,
352
+ private runSync<H extends PluginLifecycleHooks>({
353
+ strategy,
354
+ hookName,
355
+ parameters,
356
+ plugin,
357
+ }: {
358
+ strategy: Strategy
359
+ hookName: H
360
+ parameters: Parameters<PluginLifecycle[H]>
227
361
  plugin: KubbPlugin
228
- ): ReturnType<PluginLifecycle[H]> | Error {
362
+ }): ReturnType<PluginLifecycle[H]> {
229
363
  const hook = plugin[hookName]!
230
364
 
231
365
  // const context = this.pluginContexts.get(plugin)!;
232
366
 
233
367
  try {
234
368
  // eslint-disable-next-line @typescript-eslint/ban-types
369
+ if (typeof hook !== 'function') {
370
+ return hook
371
+ }
372
+
235
373
  return (hook as Function).apply(this.core.api, parameters)
236
- } catch (error) {
237
- return error as Error
374
+ } catch (e) {
375
+ this.catcher<H>(e as Error, plugin, hookName)
376
+ return null as ReturnType<PluginLifecycle[H]>
238
377
  }
239
378
  }
240
379
 
package/src/plugin.ts CHANGED
@@ -30,7 +30,8 @@ export function createPlugin<T extends PluginFactoryOptions = PluginFactoryOptio
30
30
  type Options = {
31
31
  config: PluginContext['config']
32
32
  fileManager: FileManager
33
- resolveId: PluginContext['resolveId']
33
+ resolvePath: PluginContext['resolvePath']
34
+ resolveName: PluginContext['resolveName']
34
35
  load: PluginContext['load']
35
36
  }
36
37
 
@@ -40,7 +41,7 @@ export type CorePluginOptions = PluginFactoryOptions<Options, false, PluginConte
40
41
  export const name = 'core' as const
41
42
 
42
43
  export const definePlugin = createPlugin<CorePluginOptions>((options) => {
43
- const { fileManager, resolveId, load } = options
44
+ const { fileManager, resolvePath, resolveName, load } = options
44
45
 
45
46
  const api: PluginContext = {
46
47
  get config() {
@@ -50,7 +51,8 @@ export const definePlugin = createPlugin<CorePluginOptions>((options) => {
50
51
  async addFile(file) {
51
52
  return fileManager.addOrAppend(file)
52
53
  },
53
- resolveId,
54
+ resolvePath,
55
+ resolveName,
54
56
  load,
55
57
  cache: createPluginCache(Object.create(null)),
56
58
  }
@@ -59,11 +61,14 @@ export const definePlugin = createPlugin<CorePluginOptions>((options) => {
59
61
  name,
60
62
  options,
61
63
  api,
62
- resolveId(fileName, directory) {
64
+ resolvePath(fileName, directory) {
63
65
  if (!directory) {
64
66
  return null
65
67
  }
66
68
  return pathParser.resolve(directory, fileName)
67
69
  },
70
+ resolveName(name) {
71
+ return name
72
+ },
68
73
  }
69
74
  })
package/src/types.ts CHANGED
@@ -1,9 +1,12 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-interface */
1
2
  import type { FileManager, File } from './managers/fileManager'
2
3
  import type { Cache } from './utils/cache'
3
4
 
5
+ export interface Register {}
6
+
4
7
  export type MaybePromise<T> = Promise<T> | T
5
8
 
6
- export type KubbUserConfig = Omit<KubbConfig, 'root'> & {
9
+ export type KubbUserConfig = Omit<KubbConfig, 'root' | 'plugins'> & {
7
10
  /**
8
11
  * Project root directory. Can be an absolute path, or a path relative from
9
12
  * the location of the config file itself.
@@ -15,7 +18,7 @@ export type KubbUserConfig = Omit<KubbConfig, 'root'> & {
15
18
  * Example: ['@kubb/swagger', { output: false }]
16
19
  * Or: createSwagger({ output: false })
17
20
  */
18
- plugins?: Array<unknown>
21
+ plugins?: KubbPlugin[] | KubbJSONPlugin[] | KubbObjectPlugin
19
22
  }
20
23
 
21
24
  /**
@@ -82,7 +85,11 @@ export type CLIOptions = {
82
85
 
83
86
  export type KubbPluginKind = 'schema' | 'controller'
84
87
 
85
- export type KubbJSONPlugin = [string, Record<string, any>]
88
+ export type KubbJSONPlugin = [plugin: keyof Register | string, options: Register[keyof Register] | object]
89
+
90
+ export type KubbObjectPlugin = {
91
+ [K in keyof Register]: Register[K] | object
92
+ }
86
93
 
87
94
  export type KubbPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions> = {
88
95
  /**
@@ -108,11 +115,11 @@ export type KubbPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOpti
108
115
  } & Partial<PluginLifecycle<TOptions>>
109
116
 
110
117
  // use of type objects
111
- export type PluginFactoryOptions<Options = unknown, Nested extends boolean = false, Api = any, ResolveIdOptions = Record<string, any>> = {
118
+ export type PluginFactoryOptions<Options = unknown, Nested extends boolean = false, Api = any, resolvePathOptions = Record<string, any>> = {
112
119
  options: Options
113
- resolveIdOptions: ResolveIdOptions
114
120
  nested: Nested
115
121
  api: Api
122
+ resolvePathOptions: resolvePathOptions
116
123
  }
117
124
 
118
125
  export type PluginLifecycle<TOptions extends PluginFactoryOptions = PluginFactoryOptions> = {
@@ -127,13 +134,21 @@ export type PluginLifecycle<TOptions extends PluginFactoryOptions = PluginFactor
127
134
  */
128
135
  buildStart: (this: PluginContext, kubbConfig: KubbConfig) => MaybePromise<void>
129
136
  /**
130
- * Resolve to an id based on importee(example: `./Pet.ts`) and directory(example: `./models`).
137
+ * Resolve to a Path based on a fileName(example: `./Pet.ts`) and directory(example: `./models`).
138
+ * Options can als be included.
139
+ * @type hookFirst
140
+ * @example ('./Pet.ts', './src/gen/') => '/src/gen/Pet.ts'
141
+ */
142
+ resolvePath: (this: Omit<PluginContext, 'addFile'>, fileName: string, directory?: string, options?: TOptions['resolvePathOptions']) => OptionalPath
143
+ /**
144
+ * Resolve to a name based on a string.
145
+ * Useful when converting to PascalCase or camelCase.
131
146
  * @type hookFirst
132
- * @example ('./Pet.ts', './src/gen/')
147
+ * @example ('pet') => 'Pet'
133
148
  */
134
- resolveId: (this: Omit<PluginContext, 'addFile'>, fileName: string, directory?: string, options?: TOptions['resolveIdOptions']) => OptionalPath
149
+ resolveName: (this: Omit<PluginContext, 'addFile'>, name: string) => string | null
135
150
  /**
136
- * Makes it possible to run async logic to override the path defined previously by `resolveId`.
151
+ * Makes it possible to run async logic to override the path defined previously by `resolvePath`.
137
152
  * @type hookFirst
138
153
  */
139
154
  load: (this: Omit<PluginContext, 'addFile'>, path: Path) => MaybePromise<TransformResult | null>
@@ -143,7 +158,7 @@ export type PluginLifecycle<TOptions extends PluginFactoryOptions = PluginFactor
143
158
  */
144
159
  transform: (this: Omit<PluginContext, 'addFile'>, source: string, path: Path) => MaybePromise<TransformResult>
145
160
  /**
146
- * Write the result to the file-system based on the id(defined by `resolveId` or changed by `load`).
161
+ * Write the result to the file-system based on the id(defined by `resolvePath` or changed by `load`).
147
162
  * @type hookParallel
148
163
  */
149
164
  writeFile: (this: Omit<PluginContext, 'addFile'>, source: string | undefined, path: Path) => MaybePromise<void>
@@ -156,26 +171,36 @@ export type PluginLifecycle<TOptions extends PluginFactoryOptions = PluginFactor
156
171
 
157
172
  export type PluginLifecycleHooks = keyof PluginLifecycle
158
173
 
159
- export type ResolveIdParams<TOptions = Record<string, any>> = {
160
- fileName: string
161
- directory?: string | undefined
174
+ export type ResolvePathParams<TOptions = Record<string, any>> = {
162
175
  /**
163
- * When set, resolveId will only call resolveId of the name of the plugin set here.
164
- * If not defined it will fall back on the resolveId of the core plugin.
176
+ * When set, resolvePath will only call resolvePath of the name of the plugin set here.
177
+ * If not defined it will fall back on the resolvePath of the core plugin.
165
178
  */
166
179
  pluginName?: string
180
+ fileName: string
181
+ directory?: string | undefined
167
182
  /**
168
- * Options to be passed to 'resolveId' 3th parameter
183
+ * Options to be passed to 'resolvePath' 3th parameter
169
184
  */
170
185
  options?: TOptions
171
186
  }
172
187
 
188
+ export type ResolveNameParams = {
189
+ /**
190
+ * When set, resolvePath will only call resolvePath of the name of the plugin set here.
191
+ * If not defined it will fall back on the resolvePath of the core plugin.
192
+ */
193
+ pluginName?: string
194
+ name: string
195
+ }
196
+
173
197
  export type PluginContext<TOptions = Record<string, any>> = {
174
198
  config: KubbConfig
175
199
  cache: Cache
176
200
  fileManager: FileManager
177
201
  addFile: (file: File) => Promise<File>
178
- resolveId: (params: ResolveIdParams<TOptions>) => MaybePromise<OptionalPath>
202
+ resolvePath: (params: ResolvePathParams<TOptions>) => OptionalPath
203
+ resolveName: (params: ResolveNameParams) => string | null
179
204
  load: (id: string) => MaybePromise<TransformResult | void>
180
205
  }
181
206
 
@@ -0,0 +1,3 @@
1
+ export function getEncodedText(text?: string): string {
2
+ return text ? text.replaceAll('`', '\\`') : ''
3
+ }
@@ -9,3 +9,5 @@ export * from './jsdoc'
9
9
  export * from './getUniqueName'
10
10
  export * from './timeout'
11
11
  export * from './queue'
12
+ export * from './getEncodedText'
13
+ export * from './renderTemplate'
@@ -1,8 +1,8 @@
1
- export function createJSDocBlockText({ comments }: { comments: Array<string | undefined> }) {
1
+ export function createJSDocBlockText({ comments }: { comments: Array<string> }): string {
2
2
  const filteredComments = comments.filter(Boolean)
3
3
 
4
4
  if (!filteredComments.length) {
5
- return undefined
5
+ return ''
6
6
  }
7
7
 
8
8
  const text = filteredComments.reduce((acc, comment) => {
@@ -0,0 +1,11 @@
1
+ export function renderTemplate<TData extends Record<string, string> = Record<string, string>>(template: string, data: TData | undefined = undefined) {
2
+ if (!data) {
3
+ return template.replace(/{{(.*?)}}/g, '')
4
+ }
5
+
6
+ return template.replace(/{{(.*?)}}/g, (match) => {
7
+ const value = data[match.split(/{{|}}/).filter(Boolean)[0].trim()]
8
+
9
+ return value || ''
10
+ })
11
+ }