@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
@@ -0,0 +1,482 @@
1
+ /* eslint-disable @typescript-eslint/no-namespace */
2
+ import crypto from 'node:crypto'
3
+ import { extname } from 'node:path'
4
+
5
+ import { print } from '@kubb/parser'
6
+ import * as factory from '@kubb/parser/factory'
7
+
8
+ import isEqual from 'lodash.isequal'
9
+ import { orderBy } from 'natural-orderby'
10
+
11
+ import { read } from './utils/read.ts'
12
+ import { timeout } from './utils/timeout.ts'
13
+ import { transformers } from './utils/transformers/index.ts'
14
+ import { write } from './utils/write.ts'
15
+ import { BarrelManager } from './BarrelManager.ts'
16
+
17
+ import type { BarrelManagerOptions } from './BarrelManager.ts'
18
+ import type { GreaterThan, KubbPlugin } from './types.ts'
19
+ import type { Queue, QueueJob } from './utils/Queue.ts'
20
+
21
+ type BasePath<T extends string = string> = `${T}/`
22
+
23
+ export namespace KubbFile {
24
+ export type Import = {
25
+ name: string | Array<string>
26
+ path: string
27
+ isTypeOnly?: boolean
28
+ }
29
+
30
+ export type Export = {
31
+ name?: string | Array<string>
32
+ path: string
33
+ isTypeOnly?: boolean
34
+ asAlias?: boolean
35
+ }
36
+
37
+ export declare const dataTagSymbol: unique symbol
38
+ export type DataTag<Type, Value> = Type & {
39
+ [dataTagSymbol]: Value
40
+ }
41
+
42
+ export type UUID = string
43
+ export type Source = string
44
+
45
+ export type Extname = '.ts' | '.js' | '.tsx' | '.json' | `.${string}`
46
+
47
+ export type Mode = 'file' | 'directory'
48
+
49
+ export type BaseName = `${string}${Extname}`
50
+
51
+ export type Path = string
52
+
53
+ export type AdvancedPath<T extends BaseName = BaseName> = `${BasePath}${T}`
54
+
55
+ export type OptionalPath = Path | undefined | null
56
+
57
+ export type FileMetaBase = {
58
+ pluginKey?: KubbPlugin['key']
59
+ }
60
+
61
+ export type File<
62
+ TMeta extends FileMetaBase = FileMetaBase,
63
+ TBaseName extends BaseName = BaseName,
64
+ > = {
65
+ /**
66
+ * Unique identifier to reuse later
67
+ * @default crypto.randomUUID()
68
+ */
69
+ id?: string
70
+ /**
71
+ * Name to be used to dynamicly create the baseName(based on input.path)
72
+ * Based on UNIX basename
73
+ * @link https://nodejs.org/api/path.html#pathbasenamepath-suffix
74
+ */
75
+ baseName: TBaseName
76
+ /**
77
+ * Path will be full qualified path to a specified file
78
+ */
79
+ path: AdvancedPath<TBaseName> | Path
80
+ source: Source
81
+ imports?: Import[]
82
+ exports?: Export[]
83
+ /**
84
+ * This will call fileManager.add instead of fileManager.addOrAppend, adding the source when the files already exists
85
+ * This will also ignore the combinefiles utils
86
+ * @default `false`
87
+ */
88
+ override?: boolean
89
+ meta?: TMeta
90
+ /**
91
+ * This will override `process.env[key]` inside the `source`, see `getFileSource`.
92
+ */
93
+ env?: NodeJS.ProcessEnv
94
+ validate?: boolean
95
+ }
96
+
97
+ export type ResolvedFile<
98
+ TMeta extends FileMetaBase = FileMetaBase,
99
+ TBaseName extends BaseName = BaseName,
100
+ > = KubbFile.File<TMeta, TBaseName> & {
101
+ /**
102
+ * @default crypto.randomUUID()
103
+ */
104
+ id: UUID
105
+ }
106
+ }
107
+
108
+ type CacheItem = KubbFile.ResolvedFile & {
109
+ cancel?: () => void
110
+ }
111
+
112
+ type AddResult<T extends Array<KubbFile.File>> = Promise<
113
+ Awaited<GreaterThan<T['length'], 1> extends true ? Promise<KubbFile.ResolvedFile[]> : Promise<KubbFile.ResolvedFile>>
114
+ >
115
+
116
+ type AddIndexesProps = {
117
+ root: KubbFile.Path
118
+ extName?: KubbFile.Extname
119
+ options?: BarrelManagerOptions
120
+ meta?: KubbFile.File['meta']
121
+ }
122
+
123
+ type Options = {
124
+ queue?: Queue
125
+ task?: QueueJob<KubbFile.ResolvedFile>
126
+ /**
127
+ * Timeout between writes
128
+ */
129
+ timeout?: number
130
+ }
131
+
132
+ export class FileManager {
133
+ #cache: Map<KubbFile.Path, CacheItem[]> = new Map()
134
+
135
+ #task?: QueueJob<KubbFile.ResolvedFile>
136
+ #isWriting = false
137
+ /**
138
+ * Timeout between writes
139
+ */
140
+ #timeout: number = 0
141
+ #queue?: Queue
142
+
143
+ constructor(options?: Options) {
144
+ if (options) {
145
+ this.#task = options.task
146
+ this.#queue = options.queue
147
+ this.#timeout = options.timeout || 0
148
+ }
149
+
150
+ return this
151
+ }
152
+
153
+ get files(): Array<KubbFile.File> {
154
+ const files: Array<KubbFile.File> = []
155
+ this.#cache.forEach((item) => {
156
+ files.push(...item.flat(1))
157
+ })
158
+
159
+ return files
160
+ }
161
+ get isExecuting(): boolean {
162
+ return this.#queue?.hasJobs ?? this.#isWriting ?? false
163
+ }
164
+
165
+ #validate(file: KubbFile.File): void {
166
+ if (!file.validate) {
167
+ return
168
+ }
169
+
170
+ if (!file.path.toLowerCase().endsWith(file.baseName.toLowerCase())) {
171
+ throw new Error(`${file.path} should end with the baseName ${file.baseName}`)
172
+ }
173
+ }
174
+
175
+ async add<T extends Array<KubbFile.File> = Array<KubbFile.File>>(
176
+ ...files: T
177
+ ): AddResult<T> {
178
+ const promises = files.map((file) => {
179
+ this.#validate(file)
180
+
181
+ if (file.override) {
182
+ return this.#add(file)
183
+ }
184
+
185
+ return this.#addOrAppend(file)
186
+ })
187
+
188
+ const resolvedFiles = await Promise.all(promises)
189
+
190
+ if (files.length > 1) {
191
+ return resolvedFiles as unknown as AddResult<T>
192
+ }
193
+
194
+ return resolvedFiles[0] as unknown as AddResult<T>
195
+ }
196
+
197
+ async #add(file: KubbFile.File): Promise<KubbFile.ResolvedFile> {
198
+ const controller = new AbortController()
199
+ const resolvedFile: KubbFile.ResolvedFile = { id: crypto.randomUUID(), ...file }
200
+
201
+ this.#cache.set(resolvedFile.path, [{ cancel: () => controller.abort(), ...resolvedFile }])
202
+
203
+ if (this.#queue) {
204
+ await this.#queue.run(
205
+ async () => {
206
+ return this.#task?.(resolvedFile)
207
+ },
208
+ { controller },
209
+ )
210
+ }
211
+
212
+ return resolvedFile
213
+ }
214
+
215
+ async #addOrAppend(file: KubbFile.File): Promise<KubbFile.ResolvedFile> {
216
+ const previousCaches = this.#cache.get(file.path)
217
+ const previousCache = previousCaches ? previousCaches.at(previousCaches.length - 1) : undefined
218
+
219
+ if (previousCache) {
220
+ this.#cache.delete(previousCache.path)
221
+
222
+ return this.#add({
223
+ ...file,
224
+ source: previousCache.source && file.source ? `${previousCache.source}\n${file.source}` : '',
225
+ imports: [...(previousCache.imports || []), ...(file.imports || [])],
226
+ exports: [...(previousCache.exports || []), ...(file.exports || [])],
227
+ env: { ...(previousCache.env || {}), ...(file.env || {}) },
228
+ })
229
+ }
230
+ return this.#add(file)
231
+ }
232
+
233
+ async addIndexes({ root, extName = '.ts', meta, options = {} }: AddIndexesProps): Promise<Array<KubbFile.File> | undefined> {
234
+ const barrelManager = new BarrelManager(options)
235
+
236
+ const files = barrelManager.getIndexes(root, extName)
237
+
238
+ if (!files) {
239
+ return undefined
240
+ }
241
+
242
+ return await Promise.all(
243
+ files.map((file) => {
244
+ return this.#addOrAppend({
245
+ ...file,
246
+ meta: meta ? meta : file.meta,
247
+ })
248
+ }),
249
+ )
250
+ }
251
+
252
+ getCacheByUUID(UUID: KubbFile.UUID): KubbFile.File | undefined {
253
+ let cache: KubbFile.File | undefined
254
+
255
+ this.#cache.forEach((files) => {
256
+ cache = files.find((item) => item.id === UUID)
257
+ })
258
+ return cache
259
+ }
260
+
261
+ get(path: KubbFile.Path): Array<KubbFile.File> | undefined {
262
+ return this.#cache.get(path)
263
+ }
264
+
265
+ remove(path: KubbFile.Path): void {
266
+ const cacheItem = this.get(path)
267
+ if (!cacheItem) {
268
+ return
269
+ }
270
+
271
+ this.#cache.delete(path)
272
+ }
273
+
274
+ async write(...params: Parameters<typeof write>): Promise<string | undefined> {
275
+ if (!this.#isWriting) {
276
+ this.#isWriting = true
277
+
278
+ const text = await write(...params)
279
+
280
+ this.#isWriting = false
281
+ return text
282
+ }
283
+
284
+ await timeout(this.#timeout)
285
+
286
+ return this.write(...params)
287
+ }
288
+
289
+ async read(...params: Parameters<typeof read>): Promise<string> {
290
+ return read(...params)
291
+ }
292
+
293
+ // statics
294
+
295
+ static getSource<TMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase>(file: KubbFile.File<TMeta>): string {
296
+ if (!FileManager.isExtensionAllowed(file.baseName)) {
297
+ return file.source
298
+ }
299
+
300
+ const exports = file.exports ? combineExports(file.exports) : []
301
+ const imports = file.imports ? combineImports(file.imports, exports, file.source) : []
302
+
303
+ const importNodes = imports.map((item) => factory.createImportDeclaration({ name: item.name, path: item.path, isTypeOnly: item.isTypeOnly }))
304
+ const exportNodes = exports.map((item) =>
305
+ factory.createExportDeclaration({ name: item.name, path: item.path, isTypeOnly: item.isTypeOnly, asAlias: item.asAlias })
306
+ )
307
+
308
+ return [print([...importNodes, ...exportNodes]), getEnvSource(file.source, file.env)].join('\n')
309
+ }
310
+ static combineFiles<TMeta extends KubbFile.FileMetaBase = KubbFile.FileMetaBase>(files: Array<KubbFile.File<TMeta> | null>): Array<KubbFile.File<TMeta>> {
311
+ return files.filter(Boolean).reduce((acc, file: KubbFile.File<TMeta>) => {
312
+ const prevIndex = acc.findIndex((item) => item.path === file.path)
313
+
314
+ if (prevIndex === -1) {
315
+ return [...acc, file]
316
+ }
317
+
318
+ const prev = acc[prevIndex]
319
+
320
+ if (prev && file.override) {
321
+ acc[prevIndex] = {
322
+ imports: [],
323
+ exports: [],
324
+ ...file,
325
+ }
326
+ return acc
327
+ }
328
+
329
+ if (prev) {
330
+ acc[prevIndex] = {
331
+ ...file,
332
+ source: prev.source && file.source ? `${prev.source}\n${file.source}` : '',
333
+ imports: [...(prev.imports || []), ...(file.imports || [])],
334
+ exports: [...(prev.exports || []), ...(file.exports || [])],
335
+ env: { ...(prev.env || {}), ...(file.env || {}) },
336
+ }
337
+ }
338
+
339
+ return acc
340
+ }, [] as Array<KubbFile.File<TMeta>>)
341
+ }
342
+ static getMode(path: string | undefined | null): KubbFile.Mode {
343
+ if (!path) {
344
+ return 'directory'
345
+ }
346
+ return extname(path) ? 'file' : 'directory'
347
+ }
348
+
349
+ static get extensions(): Array<KubbFile.Extname> {
350
+ return ['.js', '.ts', '.tsx']
351
+ }
352
+
353
+ static isExtensionAllowed(baseName: string): boolean {
354
+ return FileManager.extensions.some((extension) => baseName.endsWith(extension))
355
+ }
356
+ }
357
+
358
+ export function combineExports(exports: Array<KubbFile.Export>): Array<KubbFile.Export> {
359
+ const combinedExports = orderBy(exports, [(v) => !v.isTypeOnly], ['asc']).reduce((prev, curr) => {
360
+ const name = curr.name
361
+ const prevByPath = prev.findLast((imp) => imp.path === curr.path)
362
+ const prevByPathAndIsTypeOnly = prev.findLast((imp) => imp.path === curr.path && isEqual(imp.name, name) && imp.isTypeOnly)
363
+
364
+ if (prevByPathAndIsTypeOnly) {
365
+ // we already have an export that has the same path but uses `isTypeOnly` (export type ...)
366
+ return prev
367
+ }
368
+
369
+ const uniquePrev = prev.findLast(
370
+ (imp) => imp.path === curr.path && isEqual(imp.name, name) && imp.isTypeOnly === curr.isTypeOnly && imp.asAlias === curr.asAlias,
371
+ )
372
+
373
+ if (uniquePrev || (Array.isArray(name) && !name.length) || (prevByPath?.asAlias && !curr.asAlias)) {
374
+ return prev
375
+ }
376
+
377
+ if (!prevByPath) {
378
+ return [
379
+ ...prev,
380
+ {
381
+ ...curr,
382
+ name: Array.isArray(name) ? [...new Set(name)] : name,
383
+ },
384
+ ]
385
+ }
386
+
387
+ if (prevByPath && Array.isArray(prevByPath.name) && Array.isArray(curr.name) && prevByPath.isTypeOnly === curr.isTypeOnly) {
388
+ prevByPath.name = [...new Set([...prevByPath.name, ...curr.name])]
389
+
390
+ return prev
391
+ }
392
+
393
+ return [...prev, curr]
394
+ }, [] as Array<KubbFile.Export>)
395
+
396
+ return orderBy(combinedExports, [(v) => !v.isTypeOnly, (v) => v.asAlias], ['desc', 'desc'])
397
+ }
398
+
399
+ export function combineImports(imports: Array<KubbFile.Import>, exports: Array<KubbFile.Export>, source?: string): Array<KubbFile.Import> {
400
+ const combinedImports = orderBy(imports, [(v) => !v.isTypeOnly], ['asc']).reduce((prev, curr) => {
401
+ let name = Array.isArray(curr.name) ? [...new Set(curr.name)] : curr.name
402
+
403
+ const hasImportInSource = (importName: string) => {
404
+ if (!source) {
405
+ return true
406
+ }
407
+
408
+ const checker = (name?: string) => name && !!source.includes(name)
409
+ return checker(importName) || exports.some(({ name }) => (Array.isArray(name) ? name.some(checker) : checker(name)))
410
+ }
411
+
412
+ if (Array.isArray(name)) {
413
+ name = name.filter((item) => hasImportInSource(item))
414
+ }
415
+
416
+ const prevByPath = prev.findLast((imp) => imp.path === curr.path && imp.isTypeOnly === curr.isTypeOnly)
417
+ const uniquePrev = prev.findLast((imp) => imp.path === curr.path && isEqual(imp.name, name) && imp.isTypeOnly === curr.isTypeOnly)
418
+ const prevByPathNameAndIsTypeOnly = prev.findLast((imp) => imp.path === curr.path && isEqual(imp.name, name) && imp.isTypeOnly)
419
+
420
+ if (prevByPathNameAndIsTypeOnly) {
421
+ // we already have an export that has the same path but uses `isTypeOnly` (import type ...)
422
+ return prev
423
+ }
424
+
425
+ if (uniquePrev || (Array.isArray(name) && !name.length)) {
426
+ return prev
427
+ }
428
+
429
+ if (!prevByPath) {
430
+ return [
431
+ ...prev,
432
+ {
433
+ ...curr,
434
+ name,
435
+ },
436
+ ]
437
+ }
438
+
439
+ if (prevByPath && Array.isArray(prevByPath.name) && Array.isArray(name) && prevByPath.isTypeOnly === curr.isTypeOnly) {
440
+ prevByPath.name = [...new Set([...prevByPath.name, ...name])]
441
+
442
+ return prev
443
+ }
444
+
445
+ if (!Array.isArray(name) && name && !hasImportInSource(name)) {
446
+ return prev
447
+ }
448
+
449
+ return [...prev, curr]
450
+ }, [] as Array<KubbFile.Import>)
451
+
452
+ return orderBy(combinedImports, [(v) => !v.isTypeOnly], ['desc'])
453
+ }
454
+
455
+ function getEnvSource(source: string, env: NodeJS.ProcessEnv | undefined): string {
456
+ if (!env) {
457
+ return source
458
+ }
459
+
460
+ const keys = Object.keys(env)
461
+
462
+ if (!keys.length) {
463
+ return source
464
+ }
465
+
466
+ return keys.reduce((prev, key: string) => {
467
+ const environmentValue = env[key]
468
+ const replaceBy = environmentValue ? `'${environmentValue.replaceAll('"', '')?.replaceAll("'", '')}'` : 'undefined'
469
+
470
+ if (key.toUpperCase() !== key) {
471
+ throw new TypeError(`Environment should be in upperCase for ${key}`)
472
+ }
473
+
474
+ if (typeof replaceBy === 'string') {
475
+ prev = transformers.searchAndReplace({ text: prev.replaceAll(`process.env.${key}`, replaceBy), replaceBy, prefix: 'process.env', key })
476
+ // removes `declare const ...`
477
+ prev = transformers.searchAndReplace({ text: prev.replaceAll(new RegExp(`(declare const).*\n`, 'ig'), ''), replaceBy, key })
478
+ }
479
+
480
+ return prev
481
+ }, source)
482
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Abstract class that contains the building blocks for plugins to create their own Generator
3
+ * @link idea based on https://github.com/colinhacks/zod/blob/master/src/types.ts#L137
4
+ */
5
+ export abstract class Generator<TOptions = unknown, TContext = unknown> {
6
+ #options: TOptions = {} as TOptions
7
+ #context: TContext = {} as TContext
8
+
9
+ constructor(options?: TOptions, context?: TContext) {
10
+ if (context) {
11
+ this.#context = context
12
+ }
13
+
14
+ if (options) {
15
+ this.#options = options
16
+ }
17
+
18
+ return this
19
+ }
20
+
21
+ get options(): TOptions {
22
+ return this.#options
23
+ }
24
+
25
+ get context(): TContext {
26
+ return this.#context
27
+ }
28
+
29
+ set options(options: TOptions) {
30
+ this.#options = { ...this.#options, ...options }
31
+ }
32
+
33
+ abstract build(...params: unknown[]): unknown
34
+ }
@@ -0,0 +1,163 @@
1
+ import mod from 'node:module'
2
+ import os from 'node:os'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ import { findUp, findUpSync } from 'find-up'
6
+ import { coerce, satisfies } from 'semver'
7
+
8
+ type PackageJSON = {
9
+ dependencies?: Record<string, string>
10
+ devDependencies?: Record<string, string>
11
+ }
12
+
13
+ type DependencyName = string
14
+
15
+ type DependencyVersion = string
16
+
17
+ export class PackageManager {
18
+ static #cache: Record<DependencyName, DependencyVersion> = {}
19
+
20
+ #cwd?: string
21
+ #SLASHES = new Set(['/', '\\'])
22
+ constructor(workspace?: string) {
23
+ if (workspace) {
24
+ this.#cwd = workspace
25
+ }
26
+
27
+ return this
28
+ }
29
+
30
+ set workspace(workspace: string) {
31
+ this.#cwd = workspace
32
+ }
33
+
34
+ get workspace(): string | undefined {
35
+ return this.#cwd
36
+ }
37
+
38
+ normalizeDirectory(directory: string): string {
39
+ if (!this.#SLASHES.has(directory[directory.length - 1]!)) {
40
+ return `${directory}/`
41
+ }
42
+
43
+ return directory
44
+ }
45
+
46
+ getLocation(path: string): string {
47
+ let location = path
48
+
49
+ if (this.#cwd) {
50
+ const require = mod.createRequire(this.normalizeDirectory(this.#cwd))
51
+ location = require.resolve(path)
52
+ }
53
+
54
+ return location
55
+ }
56
+
57
+ async import(path: string): Promise<any | undefined> {
58
+ try {
59
+ let location = this.getLocation(path)
60
+
61
+ if (os.platform() == 'win32') {
62
+ location = pathToFileURL(location).href
63
+ }
64
+
65
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
66
+ const module = await import(location)
67
+
68
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
69
+ return module?.default ?? module
70
+ } catch (e) {
71
+ console.log(e)
72
+ return undefined
73
+ }
74
+ }
75
+
76
+ async getPackageJSON(): Promise<PackageJSON | undefined> {
77
+ const pkgPath = await findUp(['package.json'], {
78
+ cwd: this.#cwd,
79
+ })
80
+ if (!pkgPath) {
81
+ return undefined
82
+ }
83
+
84
+ return require(pkgPath) as PackageJSON
85
+ }
86
+
87
+ getPackageJSONSync(): PackageJSON | undefined {
88
+ const pkgPath = findUpSync(['package.json'], {
89
+ cwd: this.#cwd,
90
+ })
91
+ if (!pkgPath) {
92
+ return undefined
93
+ }
94
+
95
+ return require(pkgPath) as PackageJSON
96
+ }
97
+
98
+ static setVersion(dependency: DependencyName, version: DependencyVersion): void {
99
+ PackageManager.#cache[dependency] = version
100
+ }
101
+
102
+ async getVersion(dependency: DependencyName): Promise<DependencyVersion | undefined> {
103
+ if (PackageManager.#cache[dependency]) {
104
+ return PackageManager.#cache[dependency]
105
+ }
106
+
107
+ const packageJSON = await this.getPackageJSON()
108
+
109
+ if (!packageJSON) {
110
+ return undefined
111
+ }
112
+
113
+ return packageJSON['dependencies']?.[dependency] || packageJSON['devDependencies']?.[dependency]
114
+ }
115
+
116
+ getVersionSync(dependency: DependencyName): DependencyVersion | undefined {
117
+ if (PackageManager.#cache[dependency]) {
118
+ return PackageManager.#cache[dependency]
119
+ }
120
+
121
+ const packageJSON = this.getPackageJSONSync()
122
+
123
+ if (!packageJSON) {
124
+ return undefined
125
+ }
126
+
127
+ return packageJSON['dependencies']?.[dependency] || packageJSON['devDependencies']?.[dependency]
128
+ }
129
+
130
+ async isValid(dependency: DependencyName, version: DependencyVersion): Promise<boolean> {
131
+ const packageVersion = await this.getVersion(dependency)
132
+
133
+ if (!packageVersion) {
134
+ return false
135
+ }
136
+
137
+ if (packageVersion === version) {
138
+ return true
139
+ }
140
+
141
+ const semVer = coerce(packageVersion)
142
+
143
+ if (!semVer) {
144
+ throw new Error(`${packageVersion} is not valid`)
145
+ }
146
+
147
+ return satisfies(semVer, version)
148
+ }
149
+ isValidSync(dependency: DependencyName, version: DependencyVersion): boolean {
150
+ const packageVersion = this.getVersionSync(dependency)
151
+
152
+ if (!packageVersion) {
153
+ return false
154
+ }
155
+ const semVer = coerce(packageVersion)
156
+
157
+ if (!semVer) {
158
+ throw new Error(`${packageVersion} is not valid`)
159
+ }
160
+
161
+ return satisfies(semVer, version)
162
+ }
163
+ }