@kubb/core 2.0.0-alpha.3 → 2.0.0-alpha.4

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 (52) hide show
  1. package/dist/index.cjs +62 -74
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +18 -38
  4. package/dist/index.d.ts +18 -38
  5. package/dist/index.js +62 -72
  6. package/dist/index.js.map +1 -1
  7. package/dist/utils.cjs +12 -7
  8. package/dist/utils.cjs.map +1 -1
  9. package/dist/utils.d.cts +2 -1
  10. package/dist/utils.d.ts +2 -1
  11. package/dist/utils.js +12 -7
  12. package/dist/utils.js.map +1 -1
  13. package/package.json +8 -7
  14. package/src/BarrelManager.ts +123 -0
  15. package/src/FileManager.ts +482 -0
  16. package/src/Generator.ts +34 -0
  17. package/src/PackageManager.ts +163 -0
  18. package/src/PluginManager.ts +640 -0
  19. package/src/PromiseManager.ts +48 -0
  20. package/src/SchemaGenerator.ts +8 -0
  21. package/src/build.ts +198 -0
  22. package/src/config.ts +21 -0
  23. package/src/errors.ts +12 -0
  24. package/src/index.ts +28 -0
  25. package/src/plugin.ts +80 -0
  26. package/src/types.ts +370 -0
  27. package/src/utils/EventEmitter.ts +24 -0
  28. package/src/utils/FunctionParams.ts +85 -0
  29. package/src/utils/Queue.ts +110 -0
  30. package/src/utils/TreeNode.ts +122 -0
  31. package/src/utils/URLPath.ts +128 -0
  32. package/src/utils/cache.ts +35 -0
  33. package/src/utils/clean.ts +5 -0
  34. package/src/utils/executeStrategies.ts +71 -0
  35. package/src/utils/index.ts +19 -0
  36. package/src/utils/logger.ts +76 -0
  37. package/src/utils/promise.ts +13 -0
  38. package/src/utils/randomColour.ts +39 -0
  39. package/src/utils/read.ts +68 -0
  40. package/src/utils/renderTemplate.ts +31 -0
  41. package/src/utils/throttle.ts +30 -0
  42. package/src/utils/timeout.ts +7 -0
  43. package/src/utils/transformers/combineCodes.ts +3 -0
  44. package/src/utils/transformers/createJSDocBlockText.ts +15 -0
  45. package/src/utils/transformers/escape.ts +31 -0
  46. package/src/utils/transformers/indent.ts +3 -0
  47. package/src/utils/transformers/index.ts +20 -0
  48. package/src/utils/transformers/nameSorter.ts +9 -0
  49. package/src/utils/transformers/searchAndReplace.ts +25 -0
  50. package/src/utils/transformers/transformReservedWord.ts +97 -0
  51. package/src/utils/uniqueName.ts +20 -0
  52. package/src/utils/write.ts +63 -0
package/src/types.ts ADDED
@@ -0,0 +1,370 @@
1
+ import type { FileManager, KubbFile } from './FileManager.ts'
2
+ import type { OptionsPlugins, PluginUnion } from './index.ts'
3
+ import type { PluginManager } from './PluginManager.ts'
4
+ import type { Cache } from './utils/cache.ts'
5
+ import type { Logger, LogLevel } from './utils/logger.ts'
6
+
7
+ /**
8
+ * Config used in `kubb.config.js`
9
+ *
10
+ * @example import { defineConfig } from '@kubb/core'
11
+ * export default defineConfig({
12
+ * ...
13
+ * })
14
+ */
15
+ export type KubbUserConfig = Omit<KubbConfig, 'root' | 'plugins'> & {
16
+ /**
17
+ * Project root directory. Can be an absolute path, or a path relative from
18
+ * the location of the config file itself.
19
+ * @default process.cwd()
20
+ */
21
+ root?: string
22
+ /**
23
+ * Plugin type can be KubbJSONPlugin or KubbPlugin
24
+ * Example: ['@kubb/swagger', { output: false }]
25
+ * Or: createSwagger({ output: false })
26
+ */
27
+ plugins?: Array<Omit<KubbUserPlugin, 'api'> | KubbUnionPlugins | [name: string, options: object]>
28
+ }
29
+
30
+ export type InputPath = {
31
+ /**
32
+ * Path to be used as the input. This can be an absolute path or a path relative to the `root`.
33
+ */
34
+ path: string
35
+ }
36
+
37
+ export type InputData = {
38
+ /**
39
+ * `string` or `object` containing the data.
40
+ */
41
+ data: string | unknown
42
+ }
43
+
44
+ type Input = InputPath | InputData
45
+
46
+ /**
47
+ * @private
48
+ */
49
+ export type KubbConfig<TInput = Input> = {
50
+ /**
51
+ * Optional config name to show in CLI output
52
+ */
53
+ name?: string
54
+ /**
55
+ * Project root directory. Can be an absolute path, or a path relative from
56
+ * the location of the config file itself.
57
+ * @default process.cwd()
58
+ */
59
+ root: string
60
+ input: TInput
61
+ output: {
62
+ /**
63
+ * Path to be used to export all generated files.
64
+ * This can be an absolute path, or a path relative based of the defined `root` option.
65
+ */
66
+ path: string
67
+ /**
68
+ * Clean output directory before each build.
69
+ */
70
+ clean?: boolean
71
+ /**
72
+ * Write files to the fileSystem
73
+ * This is being used for the playground.
74
+ * @default true
75
+ */
76
+ write?: boolean
77
+ }
78
+ /**
79
+ * Array of Kubb plugins to use.
80
+ * The plugin/package can forsee some options that you need to pass through.
81
+ * Sometimes a plugin is depended on another plugin, if that's the case you will get an error back from the plugin you installed.
82
+ */
83
+ plugins?: Array<KubbPlugin>
84
+ /**
85
+ * Hooks that will be called when a specific action is triggered in Kubb.
86
+ */
87
+ hooks?: {
88
+ /**
89
+ * Hook that will be triggered at the end of all executions.
90
+ * Useful for running Prettier or ESLint to format/lint your code.
91
+ */
92
+ done?: string | Array<string>
93
+ }
94
+ }
95
+
96
+ export type CLIOptions = {
97
+ /**
98
+ * Path to `kubb.config.js`
99
+ */
100
+ config?: string
101
+ /**
102
+ * Watch changes on input
103
+ */
104
+ watch?: string
105
+
106
+ /**
107
+ * Log level to report when using the CLI
108
+ *
109
+ * `silent` will hide all information that is not relevant
110
+ *
111
+ * `info` will show all information possible(not related to the PluginManager)
112
+ *
113
+ * `debug` will show all information possible(related to the PluginManager), handy for seeing logs
114
+ * @default `silent`
115
+ */
116
+ logLevel?: LogLevel
117
+ }
118
+
119
+ export type BuildOutput = {
120
+ files: FileManager['files']
121
+ pluginManager: PluginManager
122
+ /**
123
+ * Only for safeBuild
124
+ */
125
+ error?: Error
126
+ }
127
+
128
+ // plugin
129
+
130
+ export type KubbPluginKind = 'schema' | 'controller'
131
+
132
+ export type KubbUnionPlugins = PluginUnion
133
+
134
+ export type KubbObjectPlugin = keyof OptionsPlugins
135
+
136
+ export type GetPluginFactoryOptions<TPlugin extends KubbUserPlugin> = TPlugin extends KubbUserPlugin<infer X> ? X : never
137
+
138
+ export type KubbUserPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions> =
139
+ & {
140
+ /**
141
+ * Unique name used for the plugin
142
+ * @example @kubb/typescript
143
+ */
144
+ name: TOptions['name']
145
+ /**
146
+ * Internal key used when a developer uses more than one of the same plugin
147
+ * @private
148
+ */
149
+ key?: TOptions['key']
150
+ /**
151
+ * Options set for a specific plugin(see kubb.config.js), passthrough of options.
152
+ */
153
+ options: TOptions['options'] extends never ? undefined : TOptions['options']
154
+ }
155
+ & Partial<PluginLifecycle<TOptions>>
156
+ & (TOptions['api'] extends never ? {
157
+ api?: never
158
+ }
159
+ : {
160
+ api: (this: TOptions['name'] extends 'core' ? null : Omit<PluginContext, 'addFile'>) => TOptions['api']
161
+ })
162
+ & (TOptions['kind'] extends never ? {
163
+ kind?: never
164
+ }
165
+ : {
166
+ /**
167
+ * Kind/type for the plugin
168
+ * Type 'schema' can be used for JSON schema's, TypeScript types, ...
169
+ * Type 'controller' can be used to create generate API calls, React-Query hooks, Axios controllers, ...
170
+ * @default undefined
171
+ */
172
+ kind: TOptions['kind']
173
+ })
174
+
175
+ export type KubbPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions> =
176
+ & {
177
+ /**
178
+ * Unique name used for the plugin
179
+ * @example @kubb/typescript
180
+ */
181
+ name: TOptions['name']
182
+ /**
183
+ * Internal key used when a developer uses more than one of the same plugin
184
+ * @private
185
+ */
186
+ key: TOptions['key']
187
+ /**
188
+ * Options set for a specific plugin(see kubb.config.js), passthrough of options.
189
+ */
190
+ options: TOptions['options'] extends never ? undefined : TOptions['options']
191
+ /**
192
+ * Kind/type for the plugin
193
+ * Type 'schema' can be used for JSON schema's, TypeScript types, ...
194
+ * Type 'controller' can be used to create generate API calls, React-Query hooks, Axios controllers, ...
195
+ * @default undefined
196
+ */
197
+ kind?: KubbPluginKind
198
+ /**
199
+ * Define an api that can be used by other plugins, see `PluginManager' where we convert from `KubbUserPlugin` to `KubbPlugin`(used when calling `createPlugin`).
200
+ */
201
+ }
202
+ & PluginLifecycle<TOptions>
203
+ & (TOptions['api'] extends never ? {
204
+ api?: never
205
+ }
206
+ : {
207
+ api: TOptions['api']
208
+ })
209
+
210
+ // use of type objects
211
+
212
+ export type PluginFactoryOptions<
213
+ Name = string,
214
+ Kind extends KubbPluginKind = KubbPluginKind | never,
215
+ Options = unknown | never,
216
+ Nested extends boolean = false,
217
+ API = unknown | never,
218
+ resolvePathOptions = Record<string, unknown>,
219
+ > = {
220
+ name: Name
221
+ kind: Kind
222
+ /**
223
+ * Same like `QueryKey` in `@tanstack/react-query`
224
+ */
225
+ key: [kind: Kind | undefined, name: Name, identifier?: string | number]
226
+ options: Options
227
+ nested: Nested
228
+ api: API
229
+ resolvePathOptions: resolvePathOptions
230
+ }
231
+
232
+ export type PluginLifecycle<TOptions extends PluginFactoryOptions = PluginFactoryOptions> = {
233
+ /**
234
+ * Valdiate all plugins to see if their depended plugins are installed and configured.
235
+ * @type hookParallel
236
+ */
237
+ validate?: (this: Omit<PluginContext, 'addFile'>, plugins: NonNullable<KubbConfig['plugins']>) => PossiblePromise<true>
238
+ /**
239
+ * Start of the lifecycle of a plugin.
240
+ * @type hookParallel
241
+ */
242
+ buildStart?: (this: PluginContext, kubbConfig: KubbConfig) => PossiblePromise<void>
243
+ /**
244
+ * Resolve to a Path based on a baseName(example: `./Pet.ts`) and directory(example: `./models`).
245
+ * Options can als be included.
246
+ * @type hookFirst
247
+ * @example ('./Pet.ts', './src/gen/') => '/src/gen/Pet.ts'
248
+ */
249
+ resolvePath?: (this: PluginContext, baseName: string, directory?: string, options?: TOptions['resolvePathOptions']) => KubbFile.OptionalPath
250
+ /**
251
+ * Resolve to a name based on a string.
252
+ * Useful when converting to PascalCase or camelCase.
253
+ * @type hookFirst
254
+ * @example ('pet') => 'Pet'
255
+ */
256
+ resolveName?: (this: PluginContext, name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
257
+ /**
258
+ * Makes it possible to run async logic to override the path defined previously by `resolvePath`.
259
+ * @type hookFirst
260
+ */
261
+ load?: (this: Omit<PluginContext, 'addFile'>, path: KubbFile.Path) => PossiblePromise<TransformResult | null>
262
+ /**
263
+ * Transform the source-code.
264
+ * @type hookReduceArg0
265
+ */
266
+ transform?: (this: Omit<PluginContext, 'addFile'>, source: string, path: KubbFile.Path) => PossiblePromise<TransformResult>
267
+ /**
268
+ * Write the result to the file-system based on the id(defined by `resolvePath` or changed by `load`).
269
+ * @type hookParallel
270
+ */
271
+ writeFile?: (this: Omit<PluginContext, 'addFile'>, source: string | undefined, path: KubbFile.Path) => PossiblePromise<string | void>
272
+ /**
273
+ * End of the plugin lifecycle.
274
+ * @type hookParallel
275
+ */
276
+ buildEnd?: (this: PluginContext) => PossiblePromise<void>
277
+ }
278
+
279
+ export type PluginLifecycleHooks = keyof PluginLifecycle
280
+
281
+ export type PluginParameter<H extends PluginLifecycleHooks> = Parameters<Required<PluginLifecycle>[H]>
282
+
283
+ export type PluginCache = Record<string, [number, unknown]>
284
+
285
+ export type ResolvePathParams<TOptions = Record<string, unknown>> = {
286
+ pluginKey?: KubbPlugin['key']
287
+ baseName: string
288
+ directory?: string | undefined
289
+ /**
290
+ * Options to be passed to 'resolvePath' 3th parameter
291
+ */
292
+ options?: TOptions
293
+ }
294
+
295
+ export type ResolveNameParams = {
296
+ name: string
297
+ pluginKey?: KubbPlugin['key']
298
+ type?: 'file' | 'function'
299
+ }
300
+
301
+ export type PluginContext<TOptions = Record<string, unknown>> = {
302
+ config: KubbConfig
303
+ cache: Cache<PluginCache>
304
+ fileManager: FileManager
305
+ pluginManager: PluginManager
306
+ addFile: (...file: Array<KubbFile.File>) => Promise<Array<KubbFile.File>>
307
+ resolvePath: (params: ResolvePathParams<TOptions>) => KubbFile.OptionalPath
308
+ resolveName: (params: ResolveNameParams) => string
309
+ logger: Logger
310
+ /**
311
+ * All plugins
312
+ */
313
+ plugins: KubbPlugin[]
314
+ /**
315
+ * Current plugin
316
+ */
317
+ plugin: KubbPlugin
318
+ }
319
+
320
+ // null will mean clear the watcher for this key
321
+ export type TransformResult = string | null
322
+
323
+ export type AppMeta = { pluginManager: PluginManager }
324
+
325
+ // generic types
326
+
327
+ export type Prettify<T> =
328
+ & {
329
+ [K in keyof T]: T[K]
330
+ }
331
+ // eslint-disable-next-line @typescript-eslint/ban-types
332
+ & {}
333
+
334
+ /**
335
+ * TODO move to @kubb/types
336
+ * @deprecated
337
+ */
338
+ export type PossiblePromise<T> = Promise<T> | T
339
+
340
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
341
+ type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never
342
+
343
+ // TS4.0+
344
+ type Push<T extends any[], V> = [...T, V]
345
+
346
+ // TS4.1+
347
+ type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>
348
+ /**
349
+ * TODO move to @kubb/types
350
+ * @deprecated
351
+ */
352
+ export type ObjValueTuple<T, KS extends any[] = TuplifyUnion<keyof T>, R extends any[] = []> = KS extends [infer K, ...infer KT]
353
+ ? ObjValueTuple<T, KT, [...R, [name: K & keyof T, options: T[K & keyof T]]]>
354
+ : R
355
+ /**
356
+ * TODO move to @kubb/types
357
+ * @deprecated
358
+ */
359
+ export type TupleToUnion<T> = T extends Array<infer ITEMS> ? ITEMS : never
360
+
361
+ /**
362
+ * TODO move to @kubb/types
363
+ * @deprecated
364
+ */
365
+ type ArrayWithLength<T extends number, U extends any[] = []> = U['length'] extends T ? U : ArrayWithLength<T, [true, ...U]>
366
+ /**
367
+ * TODO move to @kubb/types
368
+ * @deprecated
369
+ */
370
+ export type GreaterThan<T extends number, U extends number> = ArrayWithLength<U> extends [...ArrayWithLength<T>, ...infer _] ? false : true
@@ -0,0 +1,24 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
+ import { EventEmitter as NodeEventEmitter } from 'node:events'
3
+
4
+ export class EventEmitter<TEvents extends Record<string, any>> {
5
+ constructor() {
6
+ this.#emitter.setMaxListeners(100)
7
+ }
8
+ #emitter = new NodeEventEmitter()
9
+
10
+ emit<TEventName extends keyof TEvents & string>(eventName: TEventName, ...eventArg: TEvents[TEventName]): void {
11
+ this.#emitter.emit(eventName, ...(eventArg as []))
12
+ }
13
+
14
+ on<TEventName extends keyof TEvents & string>(eventName: TEventName, handler: (...eventArg: TEvents[TEventName]) => void): void {
15
+ this.#emitter.on(eventName, handler as any)
16
+ }
17
+
18
+ off<TEventName extends keyof TEvents & string>(eventName: TEventName, handler: (...eventArg: TEvents[TEventName]) => void): void {
19
+ this.#emitter.off(eventName, handler as any)
20
+ }
21
+ removeAll(): void {
22
+ this.#emitter.removeAllListeners()
23
+ }
24
+ }
@@ -0,0 +1,85 @@
1
+ import { camelCase, camelCaseTransformMerge } from 'change-case'
2
+ import { orderBy } from 'natural-orderby'
3
+
4
+ type FunctionParamsASTWithoutType = {
5
+ name?: string
6
+ type?: string
7
+ /**
8
+ * @default true
9
+ */
10
+ required?: boolean
11
+ /**
12
+ * @default true
13
+ */
14
+ enabled?: boolean
15
+ default?: string
16
+ }
17
+
18
+ type FunctionParamsASTWithType = {
19
+ name?: never
20
+ type: string
21
+ /**
22
+ * @default true
23
+ */
24
+ required?: boolean
25
+ /**
26
+ * @default true
27
+ */
28
+ enabled?: boolean
29
+ default?: string
30
+ }
31
+
32
+ export type FunctionParamsAST = FunctionParamsASTWithoutType | FunctionParamsASTWithType
33
+ export class FunctionParams {
34
+ public type?: 'generics' | 'typed'
35
+ public items: FunctionParamsAST[] = []
36
+ constructor(type?: 'generics' | 'typed') {
37
+ this.type = type
38
+
39
+ return this
40
+ }
41
+
42
+ add(item: FunctionParamsAST | Array<FunctionParamsAST | undefined> | undefined): FunctionParams {
43
+ if (!item) {
44
+ return this
45
+ }
46
+
47
+ if (Array.isArray(item)) {
48
+ item.filter(Boolean).forEach((it) => this.items.push(it))
49
+ return this
50
+ }
51
+ this.items.push(item)
52
+
53
+ return this
54
+ }
55
+
56
+ toString(): string {
57
+ const sortedData = orderBy(this.items.filter(Boolean), [(v) => !v.default, (v) => v.required ?? true], ['desc', 'desc'])
58
+
59
+ return sortedData
60
+ .filter(({ enabled = true }) => enabled)
61
+ .reduce((acc, { name, type, required = true, ...rest }) => {
62
+ if (!name) {
63
+ // when name is not se we will use TypeScript generics
64
+ acc.push(`${type}${rest.default ? ` = ${rest.default}` : ''}`)
65
+
66
+ return acc
67
+ }
68
+ // TODO check whey we still need the camelcase here
69
+ const parameterName = name.startsWith('{') ? name : camelCase(name, { delimiter: '', transform: camelCaseTransformMerge })
70
+
71
+ if (type) {
72
+ if (required) {
73
+ acc.push(`${parameterName}: ${type}${rest.default ? ` = ${rest.default}` : ''}`)
74
+ } else {
75
+ acc.push(`${parameterName}?: ${type}`)
76
+ }
77
+ } else {
78
+ acc.push(`${parameterName}`)
79
+ }
80
+
81
+ return acc
82
+ }, [] as string[])
83
+ .join(', ')
84
+ }
85
+ }
@@ -0,0 +1,110 @@
1
+ import crypto from 'node:crypto'
2
+ import { performance } from 'node:perf_hooks'
3
+
4
+ import { EventEmitter } from './EventEmitter.ts'
5
+
6
+ export type QueueJob<T = unknown> = {
7
+ (...args: unknown[]): Promise<T | void>
8
+ }
9
+
10
+ type RunOptions = {
11
+ controller?: AbortController
12
+ name?: string
13
+ description?: string
14
+ }
15
+
16
+ type QueueItem = {
17
+ reject: <T>(reason?: T) => void
18
+ resolve: <T>(value: T | PromiseLike<T>) => void
19
+ job: QueueJob<unknown>
20
+ } & Required<RunOptions>
21
+
22
+ type Events = {
23
+ jobDone: [result: unknown]
24
+ jobFailed: [error: Error]
25
+ }
26
+
27
+ export class Queue {
28
+ #queue: QueueItem[] = []
29
+ readonly eventEmitter: EventEmitter<Events> = new EventEmitter()
30
+
31
+ #workerCount = 0
32
+
33
+ #maxParallel: number
34
+ #debug = false
35
+
36
+ constructor(maxParallel: number, debug = false) {
37
+ this.#maxParallel = maxParallel
38
+ this.#debug = debug
39
+ }
40
+
41
+ run<T>(job: QueueJob<T>, options: RunOptions = { controller: new AbortController(), name: crypto.randomUUID(), description: '' }): Promise<T> {
42
+ return new Promise<T>((resolve, reject) => {
43
+ const item = { reject, resolve, job, name: options.name, description: options.description || options.name } as QueueItem
44
+
45
+ options.controller?.signal.addEventListener('abort', () => {
46
+ this.#queue = this.#queue.filter((queueItem) => queueItem.name === item.name)
47
+
48
+ reject('Aborted')
49
+ })
50
+
51
+ this.#queue.push(item)
52
+ this.#work()
53
+ })
54
+ }
55
+
56
+ runSync<T>(job: QueueJob<T>, options: RunOptions = { controller: new AbortController(), name: crypto.randomUUID(), description: '' }): void {
57
+ new Promise<T>((resolve, reject) => {
58
+ const item = { reject, resolve, job, name: options.name, description: options.description || options.name } as QueueItem
59
+
60
+ options.controller?.signal.addEventListener('abort', () => {
61
+ this.#queue = this.#queue.filter((queueItem) => queueItem.name === item.name)
62
+ })
63
+
64
+ this.#queue.push(item)
65
+ this.#work()
66
+ })
67
+ }
68
+
69
+ get hasJobs(): boolean {
70
+ return this.#workerCount > 0 || this.#queue.length > 0
71
+ }
72
+
73
+ get count(): number {
74
+ return this.#workerCount
75
+ }
76
+
77
+ #work(): void {
78
+ if (this.#workerCount >= this.#maxParallel) {
79
+ return
80
+ }
81
+
82
+ this.#workerCount++
83
+
84
+ let entry: QueueItem | undefined
85
+ while ((entry = this.#queue.shift())) {
86
+ const { reject, resolve, job, name, description } = entry
87
+
88
+ if (this.#debug) {
89
+ performance.mark(name + '_start')
90
+ }
91
+
92
+ job()
93
+ .then((result) => {
94
+ this.eventEmitter.emit('jobDone', result)
95
+
96
+ resolve(result)
97
+
98
+ if (this.#debug) {
99
+ performance.mark(name + '_stop')
100
+ performance.measure(description, name + '_start', name + '_stop')
101
+ }
102
+ })
103
+ .catch((err) => {
104
+ this.eventEmitter.emit('jobFailed', err as Error)
105
+ reject(err)
106
+ })
107
+ }
108
+ this.#workerCount--
109
+ }
110
+ }
@@ -0,0 +1,122 @@
1
+ import dirTree from 'directory-tree'
2
+
3
+ import { FileManager } from '../FileManager.ts'
4
+
5
+ import type { DirectoryTree, DirectoryTreeOptions } from 'directory-tree'
6
+
7
+ export type TreeNodeOptions = DirectoryTreeOptions
8
+
9
+ export class TreeNode<T = unknown> {
10
+ public data: T
11
+
12
+ public parent?: TreeNode<T>
13
+
14
+ public children: Array<TreeNode<T>> = []
15
+
16
+ constructor(data: T, parent?: TreeNode<T>) {
17
+ this.data = data
18
+ this.parent = parent
19
+ return this
20
+ }
21
+
22
+ addChild(data: T): TreeNode<T> {
23
+ const child = new TreeNode(data, this)
24
+ if (!this.children) {
25
+ this.children = []
26
+ }
27
+ this.children.push(child)
28
+ return child
29
+ }
30
+
31
+ find(data?: T): TreeNode<T> | null {
32
+ if (!data) {
33
+ return null
34
+ }
35
+
36
+ if (data === this.data) {
37
+ return this
38
+ }
39
+
40
+ if (this.children?.length) {
41
+ for (let i = 0, { length } = this.children, target: TreeNode<T> | null = null; i < length; i++) {
42
+ target = this.children[i]!.find(data)
43
+ if (target) {
44
+ return target
45
+ }
46
+ }
47
+ }
48
+
49
+ return null
50
+ }
51
+
52
+ get leaves(): TreeNode<T>[] {
53
+ if (!this.children || this.children.length === 0) {
54
+ // this is a leaf
55
+ return [this]
56
+ }
57
+
58
+ // if not a leaf, return all children's leaves recursively
59
+ const leaves: TreeNode<T>[] = []
60
+ if (this.children) {
61
+ for (let i = 0, { length } = this.children; i < length; i++) {
62
+ // eslint-disable-next-line prefer-spread
63
+ leaves.push.apply(leaves, this.children[i]!.leaves)
64
+ }
65
+ }
66
+ return leaves
67
+ }
68
+
69
+ get root(): TreeNode<T> {
70
+ if (!this.parent) {
71
+ return this
72
+ }
73
+ return this.parent.root
74
+ }
75
+
76
+ forEach(callback: (treeNode: TreeNode<T>) => void): this {
77
+ if (typeof callback !== 'function') {
78
+ throw new TypeError('forEach() callback must be a function')
79
+ }
80
+
81
+ // run this node through function
82
+ callback(this)
83
+
84
+ // do the same for all children
85
+ if (this.children) {
86
+ for (let i = 0, { length } = this.children; i < length; i++) {
87
+ this.children[i]!.forEach(callback)
88
+ }
89
+ }
90
+
91
+ return this
92
+ }
93
+
94
+ public static build<T = unknown>(path: string, options: TreeNodeOptions = {}): TreeNode<T> | null {
95
+ try {
96
+ const exclude = Array.isArray(options.exclude) ? options.exclude : [options.exclude].filter(Boolean)
97
+ const filteredTree = dirTree(path, { extensions: options.extensions, exclude: [/node_modules/, ...exclude] })
98
+
99
+ if (!filteredTree) {
100
+ return null
101
+ }
102
+
103
+ const treeNode = new TreeNode({ name: filteredTree.name, path: filteredTree.path, type: filteredTree.type || FileManager.getMode(filteredTree.path) })
104
+
105
+ const recurse = (node: typeof treeNode, item: DirectoryTree) => {
106
+ const subNode = node.addChild({ name: item.name, path: item.path, type: item.type || FileManager.getMode(item.path) })
107
+
108
+ if (item.children?.length) {
109
+ item.children?.forEach((child) => {
110
+ recurse(subNode, child)
111
+ })
112
+ }
113
+ }
114
+
115
+ filteredTree.children?.forEach((child) => recurse(treeNode, child))
116
+
117
+ return treeNode as TreeNode<T>
118
+ } catch (e) {
119
+ throw new Error('Something went wrong with creating index files with the TreehNode class', { cause: e })
120
+ }
121
+ }
122
+ }