@kubb/fabric-core 0.4.1 → 0.5.1

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.
@@ -0,0 +1,284 @@
1
+ import http from 'node:http'
2
+ import type { AddressInfo } from 'node:net'
3
+ import { relative } from 'node:path'
4
+ import { Presets, SingleBar } from 'cli-progress'
5
+ import { createConsola, type LogLevel } from 'consola'
6
+ import { WebSocket, WebSocketServer } from 'ws'
7
+ import type { FabricEvents } from '../Fabric.ts'
8
+ import type * as KubbFile from '../KubbFile.ts'
9
+ import { createPlugin } from './createPlugin.ts'
10
+
11
+ type Broadcast = <T = unknown>(event: keyof FabricEvents | string, payload: T) => void
12
+
13
+ type WebSocketOptions = {
14
+ /**
15
+ * Hostname to bind the websocket server to.
16
+ * @default '127.0.0.1'
17
+ */
18
+ host?: string
19
+ /**
20
+ * Port to bind the websocket server to.
21
+ * @default 0 (random available port)
22
+ */
23
+ port?: number
24
+ }
25
+
26
+ type Options = {
27
+ /**
28
+ * Explicit consola log level.
29
+ */
30
+ level?: LogLevel
31
+ /**
32
+ * Toggle progress bar output.
33
+ * @default true
34
+ */
35
+ progress?: boolean
36
+ /**
37
+ * Toggle or configure the websocket broadcast server.
38
+ * When `true`, a websocket server is started on an ephemeral port.
39
+ * When `false`, websocket support is disabled.
40
+ * When providing an object, the server uses the supplied host and port.
41
+ * @default true
42
+ */
43
+ websocket?: boolean | WebSocketOptions
44
+ }
45
+
46
+ function normalizeAddress(address: AddressInfo): { host: string; port: number } {
47
+ const host = address.address === '::' ? '127.0.0.1' : address.address
48
+
49
+ return { host, port: address.port }
50
+ }
51
+
52
+ function serializeFile(file: KubbFile.File | KubbFile.ResolvedFile) {
53
+ return {
54
+ path: file.path,
55
+ baseName: file.baseName,
56
+ name: 'name' in file ? file.name : undefined,
57
+ extname: 'extname' in file ? file.extname : undefined,
58
+ }
59
+ }
60
+
61
+ function pluralize(word: string, count: number) {
62
+ return `${count} ${word}${count === 1 ? '' : 's'}`
63
+ }
64
+
65
+ const defaultTag = 'Fabric'
66
+
67
+ const createProgressBar = () =>
68
+ new SingleBar(
69
+ {
70
+ format: '{bar} {percentage}% | {value}/{total} | {message}',
71
+ barCompleteChar: '█',
72
+ barIncompleteChar: '░',
73
+ hideCursor: true,
74
+ clearOnComplete: true,
75
+ },
76
+ Presets.shades_grey,
77
+ )
78
+
79
+ export const loggerPlugin = createPlugin<Options>({
80
+ name: 'logger',
81
+ install(ctx, options = {}) {
82
+ const { level, websocket = true, progress = true } = options
83
+
84
+ const logger = createConsola(level !== undefined ? { level } : {}).withTag(defaultTag)
85
+
86
+ const progressBar = progress ? createProgressBar() : undefined
87
+
88
+ let server: http.Server | undefined
89
+ let wss: WebSocketServer | undefined
90
+
91
+ const broadcast: Broadcast = (event, payload) => {
92
+ if (!wss) {
93
+ return
94
+ }
95
+
96
+ const message = JSON.stringify({ event, payload })
97
+
98
+ for (const client of wss.clients) {
99
+ if (client.readyState === WebSocket.OPEN) {
100
+ client.send(message)
101
+ }
102
+ }
103
+ }
104
+
105
+ if (websocket) {
106
+ const { host = '127.0.0.1', port = 0 } = typeof websocket === 'boolean' ? {} : websocket
107
+
108
+ server = http.createServer()
109
+ wss = new WebSocketServer({ server })
110
+
111
+ server.listen(port, host, () => {
112
+ const addressInfo = server?.address()
113
+
114
+ if (addressInfo && typeof addressInfo === 'object') {
115
+ const { host: resolvedHost, port: resolvedPort } = normalizeAddress(addressInfo)
116
+ const url = `ws://${resolvedHost}:${resolvedPort}`
117
+
118
+ logger.info(`Logger websocket listening on ${url}`)
119
+ broadcast('websocket:ready', { url })
120
+ }
121
+ })
122
+
123
+ wss.on('connection', (socket) => {
124
+ logger.info('Logger websocket client connected')
125
+ socket.send(
126
+ JSON.stringify({
127
+ event: 'welcome',
128
+ payload: {
129
+ message: 'Connected to Fabric log stream',
130
+ timestamp: Date.now(),
131
+ },
132
+ }),
133
+ )
134
+ })
135
+
136
+ wss.on('error', (error) => {
137
+ logger.error('Logger websocket error', error)
138
+ })
139
+ }
140
+
141
+ const formatPath = (path: string) => relative(process.cwd(), path)
142
+
143
+ ctx.on('start', async () => {
144
+ logger.start('Starting Fabric run')
145
+ broadcast('start', { timestamp: Date.now() })
146
+ })
147
+
148
+ ctx.on('render', async () => {
149
+ logger.info('Rendering application graph')
150
+ broadcast('render', { timestamp: Date.now() })
151
+ })
152
+
153
+ ctx.on('file:add', async ({ files }) => {
154
+ if (!files.length) {
155
+ return
156
+ }
157
+
158
+ logger.info(`Queued ${pluralize('file', files.length)}`)
159
+ broadcast('file:add', {
160
+ files: files.map(serializeFile),
161
+ })
162
+ })
163
+
164
+ ctx.on('file:resolve:path', async ({ file }) => {
165
+ logger.info(`Resolving path for ${formatPath(file.path)}`)
166
+ broadcast('file:resolve:path', { file: serializeFile(file) })
167
+ })
168
+
169
+ ctx.on('file:resolve:name', async ({ file }) => {
170
+ logger.info(`Resolving name for ${formatPath(file.path)}`)
171
+ broadcast('file:resolve:name', { file: serializeFile(file) })
172
+ })
173
+
174
+ ctx.on('process:start', async ({ files }) => {
175
+ logger.start(`Processing ${pluralize('file', files.length)}`)
176
+ broadcast('process:start', { total: files.length, timestamp: Date.now() })
177
+
178
+ if (progressBar) {
179
+ logger.pauseLogs()
180
+ progressBar.start(files.length, 0, { message: 'Starting...' })
181
+ }
182
+ })
183
+
184
+ ctx.on('file:start', async ({ file, index, total }) => {
185
+ logger.info(`Processing [${index + 1}/${total}] ${formatPath(file.path)}`)
186
+ broadcast('file:start', {
187
+ index,
188
+ total,
189
+ file: serializeFile(file),
190
+ })
191
+ })
192
+
193
+ ctx.on('process:progress', async ({ processed, total, percentage, file }) => {
194
+ const formattedPercentage = Number.isFinite(percentage) ? percentage.toFixed(1) : '0.0'
195
+
196
+ logger.info(`Progress ${formattedPercentage}% (${processed}/${total}) → ${formatPath(file.path)}`)
197
+ broadcast('process:progress', {
198
+ processed,
199
+ total,
200
+ percentage,
201
+ file: serializeFile(file),
202
+ })
203
+
204
+ if (progressBar) {
205
+ progressBar.increment(1, { message: `Writing ${formatPath(file.path)}` })
206
+ }
207
+ })
208
+
209
+ ctx.on('file:end', async ({ file, index, total }) => {
210
+ logger.success(`Finished [${index + 1}/${total}] ${formatPath(file.path)}`)
211
+ broadcast('file:end', {
212
+ index,
213
+ total,
214
+ file: serializeFile(file),
215
+ })
216
+ })
217
+
218
+ ctx.on('write:start', async ({ files }) => {
219
+ logger.start(`Writing ${pluralize('file', files.length)} to disk`)
220
+ broadcast('write:start', {
221
+ files: files.map(serializeFile),
222
+ })
223
+ })
224
+
225
+ ctx.on('write:end', async ({ files }) => {
226
+ logger.success(`Written ${pluralize('file', files.length)} to disk`)
227
+ broadcast('write:end', {
228
+ files: files.map(serializeFile),
229
+ })
230
+ })
231
+
232
+ ctx.on('process:end', async ({ files }) => {
233
+ logger.success(`Processed ${pluralize('file', files.length)}`)
234
+ broadcast('process:end', { total: files.length, timestamp: Date.now() })
235
+
236
+ if (progressBar) {
237
+ progressBar.update(files.length, { message: 'Done ✅' })
238
+ progressBar.stop()
239
+
240
+ logger.resumeLogs()
241
+ }
242
+ })
243
+
244
+ ctx.on('end', async () => {
245
+ logger.success('Fabric run completed')
246
+ broadcast('end', { timestamp: Date.now() })
247
+
248
+ if (progressBar) {
249
+ progressBar.stop()
250
+ logger.resumeLogs()
251
+ }
252
+
253
+ const closures: Array<Promise<void>> = []
254
+
255
+ if (wss) {
256
+ const wsServer = wss
257
+
258
+ closures.push(
259
+ new Promise((resolve) => {
260
+ for (const client of wsServer.clients) {
261
+ client.close()
262
+ }
263
+ wsServer.close(() => resolve())
264
+ }),
265
+ )
266
+ }
267
+
268
+ if (server) {
269
+ const httpServer = server
270
+
271
+ closures.push(
272
+ new Promise((resolve) => {
273
+ httpServer.close(() => resolve())
274
+ }),
275
+ )
276
+ }
277
+
278
+ if (closures.length) {
279
+ await Promise.allSettled(closures)
280
+ logger.info('Logger websocket closed')
281
+ }
282
+ })
283
+ },
284
+ })
@@ -1,34 +0,0 @@
1
- import { relative } from 'node:path'
2
- import process from 'node:process'
3
- import { Presets, SingleBar } from 'cli-progress'
4
- import { createPlugin } from './createPlugin.ts'
5
-
6
- export const progressPlugin = createPlugin({
7
- name: 'progress',
8
- install(ctx) {
9
- const progressBar = new SingleBar(
10
- {
11
- format: '{bar} {percentage}% | {value}/{total} | {message}',
12
- barCompleteChar: '█',
13
- barIncompleteChar: '░',
14
- hideCursor: true,
15
- clearOnComplete: true,
16
- },
17
- Presets.shades_grey,
18
- )
19
-
20
- ctx.on('process:start', async ({ files }) => {
21
- progressBar.start(files.length, 0, { message: 'Starting...' })
22
- })
23
-
24
- ctx.on('process:progress', async ({ file }) => {
25
- const message = `Writing ${relative(process.cwd(), file.path)}`
26
- progressBar.increment(1, { message })
27
- })
28
-
29
- ctx.on('process:end', async ({ files }) => {
30
- progressBar.update(files.length, { message: 'Done ✅' })
31
- progressBar.stop()
32
- })
33
- },
34
- })