@kubb/fabric-core 0.1.0 → 0.1.2

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 (75) hide show
  1. package/dist/App-DZuROf6f.d.ts +292 -0
  2. package/dist/App-zyf9KG3p.d.cts +292 -0
  3. package/dist/chunk-CUT6urMc.cjs +30 -0
  4. package/dist/defineApp-D3B0bU-z.d.cts +14 -0
  5. package/dist/defineApp-DJVMk9lc.d.ts +14 -0
  6. package/dist/index.cjs +217 -73
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +10 -5
  9. package/dist/index.d.ts +10 -5
  10. package/dist/index.js +204 -63
  11. package/dist/index.js.map +1 -1
  12. package/dist/parsers/typescript.cjs +5 -5
  13. package/dist/parsers/typescript.d.cts +3 -51
  14. package/dist/parsers/typescript.d.ts +3 -51
  15. package/dist/parsers/typescript.js +2 -2
  16. package/dist/parsers.cjs +7 -0
  17. package/dist/parsers.d.cts +14 -0
  18. package/dist/parsers.d.ts +14 -0
  19. package/dist/parsers.js +4 -0
  20. package/dist/plugins.cjs +76 -0
  21. package/dist/plugins.cjs.map +1 -0
  22. package/dist/plugins.d.cts +28 -0
  23. package/dist/plugins.d.ts +28 -0
  24. package/dist/plugins.js +71 -0
  25. package/dist/plugins.js.map +1 -0
  26. package/dist/tsxParser-C741ZKCN.js +26 -0
  27. package/dist/tsxParser-C741ZKCN.js.map +1 -0
  28. package/dist/tsxParser-HDf_3TMc.cjs +37 -0
  29. package/dist/tsxParser-HDf_3TMc.cjs.map +1 -0
  30. package/dist/types.d.cts +2 -2
  31. package/dist/types.d.ts +2 -2
  32. package/dist/{parser-CWB_OBtr.js → typescriptParser-BBGeFKlP.js} +51 -98
  33. package/dist/typescriptParser-BBGeFKlP.js.map +1 -0
  34. package/dist/typescriptParser-BBbbmG5W.cjs +171 -0
  35. package/dist/typescriptParser-BBbbmG5W.cjs.map +1 -0
  36. package/dist/typescriptParser-C-sBy1iR.d.cts +50 -0
  37. package/dist/typescriptParser-CtMmz0UV.d.ts +50 -0
  38. package/package.json +13 -6
  39. package/src/App.ts +91 -0
  40. package/src/FileManager.ts +14 -193
  41. package/src/FileProcessor.ts +89 -0
  42. package/src/createFile.ts +167 -0
  43. package/src/defineApp.ts +49 -74
  44. package/src/index.ts +3 -1
  45. package/src/parsers/createParser.ts +8 -0
  46. package/src/parsers/defaultParser.ts +10 -0
  47. package/src/parsers/index.ts +5 -0
  48. package/src/parsers/tsxParser.ts +11 -0
  49. package/src/parsers/types.ts +22 -0
  50. package/src/parsers/{typescript.ts → typescriptParser.ts} +8 -4
  51. package/src/plugins/createPlugin.ts +10 -0
  52. package/src/plugins/fsPlugin.ts +112 -0
  53. package/src/plugins/index.ts +3 -0
  54. package/src/plugins/types.ts +15 -0
  55. package/src/types.ts +4 -1
  56. package/src/utils/AsyncEventEmitter.ts +37 -0
  57. package/src/utils/EventEmitter.ts +23 -0
  58. package/src/utils/getRelativePath.ts +32 -0
  59. package/src/utils/trimExtName.ts +3 -0
  60. package/dist/KubbFile-BrN7Wwp6.d.cts +0 -119
  61. package/dist/KubbFile-BzVkcu9M.d.ts +0 -119
  62. package/dist/defineApp-Bg7JewJQ.d.ts +0 -62
  63. package/dist/defineApp-DKW3IRO8.d.cts +0 -62
  64. package/dist/parser-CWB_OBtr.js.map +0 -1
  65. package/dist/parser-D64DdV1v.d.cts +0 -21
  66. package/dist/parser-QF8j8-pj.cjs +0 -260
  67. package/dist/parser-QF8j8-pj.cjs.map +0 -1
  68. package/dist/parser-yYqnryUV.d.ts +0 -21
  69. package/dist/parsers/tsx.cjs +0 -3
  70. package/dist/parsers/tsx.d.cts +0 -8
  71. package/dist/parsers/tsx.d.ts +0 -8
  72. package/dist/parsers/tsx.js +0 -3
  73. package/src/fs.ts +0 -167
  74. package/src/parsers/parser.ts +0 -56
  75. package/src/parsers/tsx.ts +0 -8
@@ -0,0 +1,89 @@
1
+ import type * as KubbFile from './KubbFile.ts'
2
+ import pLimit from 'p-limit'
3
+ import path from 'node:path'
4
+
5
+ import type { Parser } from './parsers/types.ts'
6
+ import { defaultParser } from './parsers/defaultParser.ts'
7
+ import { AsyncEventEmitter } from './utils/AsyncEventEmitter.ts'
8
+ import type { AppEvents } from './App.ts'
9
+ import { typescriptParser } from './parsers/typescriptParser.ts'
10
+ import { tsxParser } from './parsers/tsxParser.ts'
11
+
12
+ export type ProcessFilesProps = {
13
+ parsers?: Set<Parser>
14
+ extension?: Record<KubbFile.Extname, KubbFile.Extname | ''>
15
+ dryRun?: boolean
16
+ }
17
+
18
+ type GetParseOptions = {
19
+ parsers?: Set<Parser>
20
+ extname?: KubbFile.Extname
21
+ }
22
+
23
+ type Options = {
24
+ events?: AsyncEventEmitter<AppEvents>
25
+ }
26
+
27
+ export class FileProcessor {
28
+ #limit = pLimit(100)
29
+ events: AsyncEventEmitter<AppEvents>
30
+
31
+ constructor({ events = new AsyncEventEmitter<AppEvents>() }: Options = {}) {
32
+ this.events = events
33
+
34
+ return this
35
+ }
36
+
37
+ get #defaultParser(): Set<Parser> {
38
+ console.warn(`[parser] using default parsers, please consider using the "use" method to add custom parsers.`)
39
+
40
+ return new Set<Parser>([typescriptParser, tsxParser, defaultParser])
41
+ }
42
+
43
+ async parse(file: KubbFile.ResolvedFile, { parsers = this.#defaultParser, extname }: GetParseOptions = {}): Promise<string> {
44
+ if (!extname) {
45
+ console.warn('[parser] No extname found, default parser will be used')
46
+ return defaultParser.parse(file, { extname })
47
+ }
48
+
49
+ const parser = [...parsers].find((item) => item.extNames?.includes(extname))
50
+
51
+ if (!parser) {
52
+ console.warn(`[parser] No parser found for ${extname}, default parser will be used`)
53
+
54
+ return defaultParser.parse(file, { extname })
55
+ }
56
+
57
+ return parser.parse(file, { extname })
58
+ }
59
+
60
+ async run(files: Array<KubbFile.ResolvedFile>, { parsers, dryRun, extension }: ProcessFilesProps = {}): Promise<KubbFile.ResolvedFile[]> {
61
+ await this.events.emit('process:start', { files })
62
+
63
+ let processed = 0
64
+ const total = files.length
65
+
66
+ const promises = files.map((resolvedFile, index) =>
67
+ this.#limit(async () => {
68
+ const extname = extension?.[resolvedFile.extname] || (path.extname(resolvedFile.path) as KubbFile.Extname)
69
+
70
+ await this.events.emit('file:start', { file: resolvedFile, index, total })
71
+
72
+ if (!dryRun) {
73
+ const source = await this.parse(resolvedFile, { extname, parsers })
74
+ await this.events.emit('process:progress', { file: resolvedFile, source, processed, percentage: (processed / total) * 100, total })
75
+ }
76
+
77
+ await this.events.emit('file:end', { file: resolvedFile, index, total })
78
+
79
+ processed++
80
+ }),
81
+ )
82
+
83
+ await Promise.all(promises)
84
+
85
+ await this.events.emit('process:end', { files })
86
+
87
+ return files
88
+ }
89
+ }
@@ -0,0 +1,167 @@
1
+ import type * as KubbFile from './KubbFile.ts'
2
+ import { trimExtName } from './utils/trimExtName.ts'
3
+ import { createHash } from 'node:crypto'
4
+ import path from 'node:path'
5
+ import { isDeepEqual, uniqueBy } from 'remeda'
6
+ import { orderBy } from 'natural-orderby'
7
+
8
+ function hashObject(obj: Record<string, unknown>): string {
9
+ const str = JSON.stringify(obj, Object.keys(obj).sort())
10
+ return createHash('sha256').update(str).digest('hex')
11
+ }
12
+
13
+ export function combineSources(sources: Array<KubbFile.Source>): Array<KubbFile.Source> {
14
+ return uniqueBy(sources, (obj) => [obj.name, obj.isExportable, obj.isTypeOnly] as const)
15
+ }
16
+
17
+ export function combineExports(exports: Array<KubbFile.Export>): Array<KubbFile.Export> {
18
+ return orderBy(exports, [
19
+ (v) => !!Array.isArray(v.name),
20
+ (v) => !v.isTypeOnly,
21
+ (v) => v.path,
22
+ (v) => !!v.name,
23
+ (v) => (Array.isArray(v.name) ? orderBy(v.name) : v.name),
24
+ ]).reduce(
25
+ (prev, curr) => {
26
+ const name = curr.name
27
+ const prevByPath = prev.findLast((imp) => imp.path === curr.path)
28
+ const prevByPathAndIsTypeOnly = prev.findLast((imp) => imp.path === curr.path && isDeepEqual(imp.name, name) && imp.isTypeOnly)
29
+
30
+ if (prevByPathAndIsTypeOnly) {
31
+ // we already have an export that has the same path but uses `isTypeOnly` (export type ...)
32
+ return prev
33
+ }
34
+
35
+ const uniquePrev = prev.findLast(
36
+ (imp) => imp.path === curr.path && isDeepEqual(imp.name, name) && imp.isTypeOnly === curr.isTypeOnly && imp.asAlias === curr.asAlias,
37
+ )
38
+
39
+ // we already have an item that was unique enough or name field is empty or prev asAlias is set but current has no changes
40
+ if (uniquePrev || (Array.isArray(name) && !name.length) || (prevByPath?.asAlias && !curr.asAlias)) {
41
+ return prev
42
+ }
43
+
44
+ if (!prevByPath) {
45
+ return [
46
+ ...prev,
47
+ {
48
+ ...curr,
49
+ name: Array.isArray(name) ? [...new Set(name)] : name,
50
+ },
51
+ ]
52
+ }
53
+
54
+ // merge all names when prev and current both have the same isTypeOnly set
55
+ if (prevByPath && Array.isArray(prevByPath.name) && Array.isArray(curr.name) && prevByPath.isTypeOnly === curr.isTypeOnly) {
56
+ prevByPath.name = [...new Set([...prevByPath.name, ...curr.name])]
57
+
58
+ return prev
59
+ }
60
+
61
+ return [...prev, curr]
62
+ },
63
+ [] as Array<KubbFile.Export>,
64
+ )
65
+ }
66
+
67
+ export function combineImports(imports: Array<KubbFile.Import>, exports: Array<KubbFile.Export>, source?: string): Array<KubbFile.Import> {
68
+ return orderBy(imports, [
69
+ (v) => !!Array.isArray(v.name),
70
+ (v) => !v.isTypeOnly,
71
+ (v) => v.path,
72
+ (v) => !!v.name,
73
+ (v) => (Array.isArray(v.name) ? orderBy(v.name) : v.name),
74
+ ]).reduce(
75
+ (prev, curr) => {
76
+ let name = Array.isArray(curr.name) ? [...new Set(curr.name)] : curr.name
77
+
78
+ const hasImportInSource = (importName: string) => {
79
+ if (!source) {
80
+ return true
81
+ }
82
+
83
+ const checker = (name?: string) => {
84
+ return name && source.includes(name)
85
+ }
86
+
87
+ return checker(importName) || exports.some(({ name }) => (Array.isArray(name) ? name.some(checker) : checker(name)))
88
+ }
89
+
90
+ if (curr.path === curr.root) {
91
+ // root and path are the same file, remove the "./" import
92
+ return prev
93
+ }
94
+
95
+ // merge all names and check if the importName is being used in the generated source and if not filter those imports out
96
+ if (Array.isArray(name)) {
97
+ name = name.filter((item) => (typeof item === 'string' ? hasImportInSource(item) : hasImportInSource(item.propertyName)))
98
+ }
99
+
100
+ const prevByPath = prev.findLast((imp) => imp.path === curr.path && imp.isTypeOnly === curr.isTypeOnly)
101
+ const uniquePrev = prev.findLast((imp) => imp.path === curr.path && isDeepEqual(imp.name, name) && imp.isTypeOnly === curr.isTypeOnly)
102
+ const prevByPathNameAndIsTypeOnly = prev.findLast((imp) => imp.path === curr.path && isDeepEqual(imp.name, name) && imp.isTypeOnly)
103
+
104
+ if (prevByPathNameAndIsTypeOnly) {
105
+ // we already have an export that has the same path but uses `isTypeOnly` (import type ...)
106
+ return prev
107
+ }
108
+
109
+ // already unique enough or name is empty
110
+ if (uniquePrev || (Array.isArray(name) && !name.length)) {
111
+ return prev
112
+ }
113
+
114
+ // new item, append name
115
+ if (!prevByPath) {
116
+ return [
117
+ ...prev,
118
+ {
119
+ ...curr,
120
+ name,
121
+ },
122
+ ]
123
+ }
124
+
125
+ // merge all names when prev and current both have the same isTypeOnly set
126
+ if (prevByPath && Array.isArray(prevByPath.name) && Array.isArray(name) && prevByPath.isTypeOnly === curr.isTypeOnly) {
127
+ prevByPath.name = [...new Set([...prevByPath.name, ...name])]
128
+
129
+ return prev
130
+ }
131
+
132
+ // no import was found in the source, ignore import
133
+ if (!Array.isArray(name) && name && !hasImportInSource(name)) {
134
+ return prev
135
+ }
136
+
137
+ return [...prev, curr]
138
+ },
139
+ [] as Array<KubbFile.Import>,
140
+ )
141
+ }
142
+
143
+ /**
144
+ * Helper to create a file with name and id set
145
+ */
146
+ export function createFile<TMeta extends object = object>(file: KubbFile.File<TMeta>): KubbFile.ResolvedFile<TMeta> {
147
+ const extname = path.extname(file.baseName) as KubbFile.Extname
148
+ if (!extname) {
149
+ throw new Error(`No extname found for ${file.baseName}`)
150
+ }
151
+
152
+ const source = file.sources.map((item) => item.value).join('\n\n')
153
+ const exports = file.exports?.length ? combineExports(file.exports) : []
154
+ const imports = file.imports?.length && source ? combineImports(file.imports, exports, source) : []
155
+ const sources = file.sources?.length ? combineSources(file.sources) : []
156
+
157
+ return {
158
+ ...file,
159
+ id: hashObject({ path: file.path }),
160
+ name: trimExtName(file.baseName),
161
+ extname,
162
+ imports: imports,
163
+ exports: exports,
164
+ sources: sources,
165
+ meta: file.meta || ({} as TMeta),
166
+ }
167
+ }
package/src/defineApp.ts CHANGED
@@ -1,17 +1,9 @@
1
- import type * as KubbFile from './KubbFile.ts'
2
1
  import { FileManager } from './FileManager.ts'
3
- import { isPromise } from 'remeda'
4
-
5
- const isFunction = (val: unknown): val is Function => typeof val === 'function'
6
-
7
- type Component = any
8
-
9
- type PluginInstallFunction<Options = any[]> = Options extends unknown[] ? (app: App, ...options: Options) => any : (app: App, options: Options) => any
10
-
11
- export type ObjectPlugin<Options = any[]> = {
12
- install: PluginInstallFunction<Options>
13
- }
14
- export type FunctionPlugin<Options = any[]> = PluginInstallFunction<Options> & Partial<ObjectPlugin<Options>>
2
+ import { isFunction, isPromise } from 'remeda'
3
+ import type { Plugin } from './plugins/types.ts'
4
+ import type { Parser } from './parsers/types.ts'
5
+ import { AsyncEventEmitter } from './utils/AsyncEventEmitter.ts'
6
+ import type { App, AppContext, Component, AppEvents } from './App.ts'
15
7
 
16
8
  type AppRenderer = {
17
9
  render(): Promise<void> | void
@@ -19,57 +11,27 @@ type AppRenderer = {
19
11
  waitUntilExit(): Promise<void>
20
12
  }
21
13
 
22
- export type AppContext<TOptions = unknown> = {
23
- options?: TOptions
24
- fileManager: FileManager
25
- addFile(...files: Array<KubbFile.File>): Promise<void>
26
- files: Array<KubbFile.ResolvedFile>
27
- clear: () => void
28
- }
29
-
30
14
  type RootRenderFunction<THostElement, TContext extends AppContext> = (this: TContext, container: THostElement, context: TContext) => AppRenderer
31
15
 
32
- type Plugin<Options = any[], P extends unknown[] = Options extends unknown[] ? Options : [Options]> = FunctionPlugin<P> | ObjectPlugin<P>
33
-
34
- type WriteOptions = {
35
- extension?: Record<KubbFile.Extname, KubbFile.Extname | ''>
36
- dryRun?: boolean
37
- }
38
-
39
- export interface App {
40
- _component: Component
41
- render(): Promise<void>
42
- renderToString(): Promise<string>
43
- getFiles(): Promise<Array<KubbFile.ResolvedFile>>
44
- use<Options>(plugin: Plugin<Options>, options: NoInfer<Options>): this
45
- write(options?: WriteOptions): Promise<void>
46
- addFile(...files: Array<KubbFile.File>): Promise<void>
47
- waitUntilExit(): Promise<void>
48
- }
49
-
50
16
  export type DefineApp<TContext extends AppContext> = (rootComponent?: Component, options?: TContext['options']) => App
51
17
 
52
18
  export function defineApp<THostElement, TContext extends AppContext>(instance: RootRenderFunction<THostElement, TContext>): DefineApp<TContext> {
53
19
  function createApp(rootComponent: Component, options?: TContext['options']): App {
54
- const installedPlugins = new WeakSet()
55
- const fileManager = new FileManager()
20
+ const events = new AsyncEventEmitter<AppEvents>()
21
+ const installedPlugins = new Set<Plugin>()
22
+ const installedParsers = new Set<Parser>()
23
+ const fileManager = new FileManager({ events })
56
24
  const context = {
25
+ events,
57
26
  options,
58
27
  fileManager,
59
- async addFile(...newFiles) {
60
- await fileManager.add(...newFiles)
61
- },
62
- clear() {
63
- context.fileManager.clear()
64
- },
65
- get files() {
66
- return fileManager.getFiles()
67
- },
28
+ installedPlugins,
29
+ installedParsers,
68
30
  } as TContext
69
31
 
70
32
  const { render, renderToString, waitUntilExit } = instance.call(context, rootComponent, context)
71
33
 
72
- const app: App = {
34
+ const app = {
73
35
  _component: rootComponent,
74
36
  async render() {
75
37
  if (isPromise(render)) {
@@ -81,36 +43,49 @@ export function defineApp<THostElement, TContext extends AppContext>(instance: R
81
43
  async renderToString() {
82
44
  return renderToString()
83
45
  },
84
- async getFiles() {
85
- return fileManager.getFiles()
46
+ get files() {
47
+ return fileManager.files
86
48
  },
87
49
  waitUntilExit,
88
- addFile: context.addFile,
89
- async write(
90
- options = {
91
- extension: { '.ts': '.ts' },
92
- dryRun: false,
93
- },
94
- ) {
95
- await fileManager.processFiles({
96
- extension: options.extension,
97
- dryRun: options.dryRun,
98
- })
50
+ async addFile(...newFiles) {
51
+ await fileManager.add(...newFiles)
99
52
  },
100
- use(plugin: Plugin, ...options: any[]) {
101
- if (installedPlugins.has(plugin)) {
102
- console.warn('Plugin has already been applied to target app.')
103
- } else if (plugin && isFunction(plugin.install)) {
104
- installedPlugins.add(plugin)
105
- plugin.install(app, ...options)
106
- } else if (isFunction(plugin)) {
107
- installedPlugins.add(plugin)
108
- plugin(app, ...options)
53
+ use(pluginOrParser, ...options) {
54
+ const args = Array.isArray(options) ? options : [options[0]]
55
+
56
+ if (pluginOrParser.type === 'plugin') {
57
+ if (installedPlugins.has(pluginOrParser)) {
58
+ console.warn('Plugin has already been applied to target app.')
59
+ } else {
60
+ installedPlugins.add(pluginOrParser)
61
+ }
62
+
63
+ if (pluginOrParser.override && isFunction(pluginOrParser.override)) {
64
+ const overrider = pluginOrParser.override
65
+
66
+ const extraApp = (overrider as any)(app, context, ...args)
67
+ Object.assign(app, extraApp)
68
+ }
69
+ }
70
+ if (pluginOrParser.type === 'parser') {
71
+ if (installedParsers.has(pluginOrParser)) {
72
+ console.warn('Parser has already been applied to target app.')
73
+ } else {
74
+ installedParsers.add(pluginOrParser)
75
+ }
76
+ }
77
+
78
+ if (pluginOrParser && isFunction(pluginOrParser.install)) {
79
+ const installer = pluginOrParser.install
80
+
81
+ ;(installer as any)(app, context, ...args)
109
82
  }
110
83
 
111
84
  return app
112
85
  },
113
- }
86
+ } as App
87
+
88
+ events.emit('start', { app })
114
89
 
115
90
  return app
116
91
  }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { createApp } from './createApp.ts'
2
2
  export { defineApp } from './defineApp.ts'
3
3
  export { FileManager } from './FileManager.ts'
4
- export { parseFile } from './parsers/parser.ts'
4
+ export { createFile } from './createFile.ts'
5
+ export { FileProcessor } from './FileProcessor.ts'
6
+ export type { App } from './App.ts'
@@ -0,0 +1,8 @@
1
+ import type { Parser, UserParser } from './types.ts'
2
+
3
+ export function createParser<TOptions = any[], TMeta extends object = any>(parser: UserParser<TOptions, TMeta>): Parser<TOptions, TMeta> {
4
+ return {
5
+ type: 'parser',
6
+ ...parser,
7
+ }
8
+ }
@@ -0,0 +1,10 @@
1
+ import { createParser } from './createParser.ts'
2
+
3
+ export const defaultParser = createParser({
4
+ name: 'default',
5
+ extNames: ['.json'],
6
+ install() {},
7
+ async parse(file) {
8
+ return file.sources.map((item) => item.value).join('\n\n')
9
+ },
10
+ })
@@ -0,0 +1,5 @@
1
+ export { createParser } from './createParser.ts'
2
+
3
+ export { defaultParser } from './defaultParser.ts'
4
+ export { tsxParser } from './tsxParser.ts'
5
+ export { typescriptParser } from './typescriptParser.ts'
@@ -0,0 +1,11 @@
1
+ import { typescriptParser } from './typescriptParser.ts'
2
+ import { createParser } from './createParser.ts'
3
+
4
+ export const tsxParser = createParser({
5
+ name: 'tsx',
6
+ extNames: ['.tsx', '.jsx'],
7
+ install() {},
8
+ async parse(file, options = { extname: '.tsx' }) {
9
+ return typescriptParser.parse(file, options)
10
+ },
11
+ })
@@ -0,0 +1,22 @@
1
+ import type * as KubbFile from '../KubbFile.ts'
2
+ import type { Install } from '../App.ts'
3
+
4
+ type PrintOptions = {
5
+ extname?: KubbFile.Extname
6
+ }
7
+
8
+ export type Parser<TOptions = any[], TMeta extends object = any> = {
9
+ name: string
10
+ type: 'parser'
11
+ /**
12
+ * Undefined is being used for the defaultParser
13
+ */
14
+ extNames: Array<KubbFile.Extname> | undefined
15
+ install: Install<TOptions>
16
+ /**
17
+ * Convert a file to string
18
+ */
19
+ parse(file: KubbFile.ResolvedFile<TMeta>, options: PrintOptions): Promise<string>
20
+ }
21
+
22
+ export type UserParser<TOptions = any[], TMeta extends object = any> = Omit<Parser<TOptions, TMeta>, 'type'>
@@ -1,7 +1,8 @@
1
1
  import ts from 'typescript'
2
- import { getRelativePath, trimExtName } from '../fs.ts'
2
+ import { getRelativePath } from '../utils/getRelativePath.ts'
3
+ import { trimExtName } from '../utils/trimExtName.ts'
3
4
  import path from 'node:path'
4
- import { createFileParser } from './parser.ts'
5
+ import { createParser } from './createParser.ts'
5
6
 
6
7
  const { factory } = ts
7
8
 
@@ -147,8 +148,11 @@ export function createExport({
147
148
  )
148
149
  }
149
150
 
150
- export const typeScriptParser = createFileParser({
151
- async print(file, options = { extname: '.ts' }) {
151
+ export const typescriptParser = createParser({
152
+ name: 'typescript',
153
+ extNames: ['.ts', '.js'],
154
+ install() {},
155
+ async parse(file, options = { extname: '.ts' }) {
152
156
  const source = file.sources.map((item) => item.value).join('\n\n')
153
157
 
154
158
  const importNodes = file.imports
@@ -0,0 +1,10 @@
1
+ import type { Plugin, UserPlugin } from './types.ts'
2
+
3
+ export function createPlugin<Options = any[], TAppExtension extends Record<string, any> = {}>(
4
+ plugin: UserPlugin<Options, TAppExtension>,
5
+ ): Plugin<Options, TAppExtension> {
6
+ return {
7
+ type: 'plugin',
8
+ ...plugin,
9
+ }
10
+ }
@@ -0,0 +1,112 @@
1
+ import { createPlugin } from './createPlugin.ts'
2
+ import { switcher } from 'js-runtime'
3
+ import fs from 'fs-extra'
4
+ import { resolve } from 'node:path'
5
+ import type * as KubbFile from '../KubbFile.ts'
6
+
7
+ type Options = {
8
+ /**
9
+ * Optional callback that is invoked whenever a file is written by the plugin.
10
+ * Useful for tests to observe write operations without spying on internal functions.
11
+ */
12
+ onWrite?: (path: string, data: string) => void | Promise<void>
13
+ }
14
+
15
+ export async function write(path: string, data: string, options: { sanity?: boolean } = {}): Promise<string | undefined> {
16
+ if (data.trim() === '') {
17
+ return undefined
18
+ }
19
+ return switcher(
20
+ {
21
+ node: async (path: string, data: string, { sanity }: { sanity?: boolean }) => {
22
+ try {
23
+ const oldContent = await fs.readFile(resolve(path), {
24
+ encoding: 'utf-8',
25
+ })
26
+ if (oldContent?.toString() === data?.toString()) {
27
+ return
28
+ }
29
+ } catch (_err) {
30
+ /* empty */
31
+ }
32
+
33
+ await fs.outputFile(resolve(path), data, { encoding: 'utf-8' })
34
+
35
+ if (sanity) {
36
+ const savedData = await fs.readFile(resolve(path), {
37
+ encoding: 'utf-8',
38
+ })
39
+
40
+ if (savedData?.toString() !== data?.toString()) {
41
+ throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`)
42
+ }
43
+
44
+ return savedData
45
+ }
46
+
47
+ return data
48
+ },
49
+ bun: async (path: string, data: string, { sanity }: { sanity?: boolean }) => {
50
+ try {
51
+ await Bun.write(resolve(path), data)
52
+
53
+ if (sanity) {
54
+ const file = Bun.file(resolve(path))
55
+ const savedData = await file.text()
56
+
57
+ if (savedData?.toString() !== data?.toString()) {
58
+ throw new Error(`Sanity check failed for ${path}\n\nData[${path.length}]:\n${path}\n\nSaved[${savedData.length}]:\n${savedData}\n`)
59
+ }
60
+
61
+ return savedData
62
+ }
63
+
64
+ return data
65
+ } catch (e) {
66
+ console.error(e)
67
+ }
68
+ },
69
+ },
70
+ 'node',
71
+ )(path, data.trim(), options)
72
+ }
73
+
74
+ type WriteOptions = {
75
+ extension?: Record<KubbFile.Extname, KubbFile.Extname | ''>
76
+ dryRun?: boolean
77
+ }
78
+
79
+ declare module '../index.ts' {
80
+ interface App {
81
+ write(options?: WriteOptions): Promise<void>
82
+ }
83
+ }
84
+
85
+ export const fsPlugin = createPlugin<Options, { write(options?: WriteOptions): Promise<void> }>({
86
+ name: 'fs',
87
+ scope: 'write',
88
+ async install(_app, context, options) {
89
+ context.events.on('process:progress', async ({ file, source }) => {
90
+ if (options?.onWrite) {
91
+ await options.onWrite(file.path, source)
92
+ }
93
+ await write(file.path, source, { sanity: false })
94
+ })
95
+ },
96
+ override(_app, context) {
97
+ return {
98
+ async write(
99
+ options = {
100
+ extension: { '.ts': '.ts' },
101
+ dryRun: false,
102
+ },
103
+ ) {
104
+ await context.fileManager.write({
105
+ extension: options.extension,
106
+ dryRun: options.dryRun,
107
+ parsers: context.installedParsers,
108
+ })
109
+ },
110
+ }
111
+ },
112
+ })
@@ -0,0 +1,3 @@
1
+ export { createPlugin } from './createPlugin.ts'
2
+
3
+ export { fsPlugin } from './fsPlugin.ts'
@@ -0,0 +1,15 @@
1
+ import type { Install, Override } from '../App.ts'
2
+
3
+ export type Plugin<TOptions = any[], TAppExtension extends Record<string, any> = {}> = {
4
+ name: string
5
+ type: 'plugin'
6
+ scope?: 'write' | 'read' | (string & {})
7
+ install: Install<TOptions> | Promise<Install<TOptions>>
8
+ /**
9
+ * Runtime app overrides or extensions.
10
+ * Merged into the app instance after install.
11
+ */
12
+ override?: Override<TOptions, TAppExtension>
13
+ }
14
+
15
+ export type UserPlugin<TOptions = any[], TAppExtension extends Record<string, any> = {}> = Omit<Plugin<TOptions, TAppExtension>, 'type'>
package/src/types.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export * as KubbFile from './KubbFile.ts'
2
- export type { DefineApp, AppContext, App } from './defineApp.ts'
2
+ export type { DefineApp } from './defineApp.ts'
3
+ export type { AppContext } from './App.ts'
4
+
5
+ export type { App } from './App.ts'