@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.
- package/README.md +10 -6
- package/dist/plugins.cjs +173 -20
- package/dist/plugins.cjs.map +1 -1
- package/dist/plugins.d.cts +42 -10
- package/dist/plugins.d.ts +42 -10
- package/dist/plugins.js +173 -19
- package/dist/plugins.js.map +1 -1
- package/package.json +6 -3
- package/src/plugins/index.ts +1 -1
- package/src/plugins/loggerPlugin.ts +284 -0
- package/src/plugins/progressPlugin.ts +0 -34
|
@@ -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
|
-
})
|