@kwirthmagnify/kwirth-sender-tee 0.1.4 → 0.1.6

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.
@@ -27,8 +27,12 @@ module.exports = __toCommonJS(index_exports);
27
27
  var TeeSender = class {
28
28
  constructor() {
29
29
  this.id = "tee";
30
+ this.senderType = "output";
30
31
  this.configs = /* @__PURE__ */ new Map();
31
32
  }
33
+ getNodeMeta() {
34
+ return { label: "Tee", icon: "CallSplit" };
35
+ }
32
36
  addConfig(config) {
33
37
  const tc = config;
34
38
  if (!Array.isArray(tc.targets)) tc.targets = [];
package/package.json CHANGED
@@ -1,25 +1,7 @@
1
- {
2
- "id": "tee",
3
- "name": "@kwirthmagnify/kwirth-sender-tee",
4
- "publisher": "@kwirthmagnify",
5
- "version": "0.1.4",
6
- "displayName": "Tee Sender",
7
- "description": "Tee sender plugin for Kwirth — fans out an incoming message to multiple downstream senders simultaneously, acting as an intermediate pipeline node",
8
- "type": "module",
9
- "scripts": {
10
- "build": "node build.mjs",
11
- "watch": "node watch.mjs"
12
- },
13
- "dependencies": {
14
- "@kwirthmagnify/kwirth-common-back": "^0.5.11"
15
- },
16
- "devDependencies": {
17
- "@mui/icons-material": "^7.1.2",
18
- "@mui/material": "^7.1.2",
19
- "@types/node": "^20.12.13",
20
- "@types/react": "^19.2.16",
21
- "@types/react-dom": "^19.2.3",
22
- "esbuild": "^0.27.2",
23
- "typescript": "^5.4.0"
24
- }
25
- }
1
+ {
2
+ "id": "tee",
3
+ "name": "@kwirthmagnify/kwirth-sender-tee",
4
+ "displayName": "Tee Sender",
5
+ "version": "0.1.6",
6
+ "description": "Tee sender plugin for Kwirth — fans out an incoming message to multiple downstream senders simultaneously, acting as an intermediate pipeline node"
7
+ }
package/README.md DELETED
@@ -1,79 +0,0 @@
1
- # Tee Sender
2
-
3
- Sender intermedio que recibe un mensaje y lo reenvía en paralelo a múltiples senders de destino configurados, sin hacer ningún envío final él mismo. Funciona como el comando Unix `tee`: lo que entra, sale por todos los canales a la vez.
4
-
5
- ## Cuándo usarlo
6
-
7
- - Enviar la misma alerta simultáneamente a consola y a fichero.
8
- - Construir pipelines de notificación: un único punto de entrada que abanica hacia varios destinos.
9
- - Encadenar tees para topologías más complejas (un tee apuntando a otro tee).
10
-
11
- ## Configuración
12
-
13
- Cada config tiene un campo `targets`: lista de pares `(senderId, configName)` que identifican el sender y la config de destino.
14
-
15
- | Campo | Tipo | Requerido | Descripción |
16
- |-----------|-----------------------------------|-----------|------------------------------------------------|
17
- | `name` | `string` | Sí | Nombre de esta config |
18
- | `targets` | `Array<{senderId, configName}>` | Sí | Lista de senders de destino |
19
-
20
- ### Estructura de un target
21
-
22
- ```json
23
- {
24
- "senderId": "console", // id del sender destino (ej: "console", "file", "tee")
25
- "configName": "mi-console" // nombre de la config ya registrada en ese sender
26
- }
27
- ```
28
-
29
- ## Ejemplos
30
-
31
- ### Fan-out a consola y fichero
32
-
33
- Prerequisito: tener configurados un `ConsoleSender` con config `"alerts-console"` y un `FileSender` con config `"alerts-file"`.
34
-
35
- ```json
36
- {
37
- "name": "alerts-tee",
38
- "targets": [
39
- { "senderId": "console", "configName": "alerts-console" },
40
- { "senderId": "file", "configName": "alerts-file" }
41
- ]
42
- }
43
- ```
44
-
45
- Invocar:
46
- ```ts
47
- senderAccess.send('tee', 'alerts-tee', {
48
- level: 'error',
49
- subject: 'Pod crash',
50
- body: 'nginx-7d4f crashed in namespace prod'
51
- })
52
- ```
53
-
54
- El mensaje llega al mismo tiempo a stdout y al fichero de log.
55
-
56
- ### Cadena de tees
57
-
58
- ```
59
- tee-main
60
- ├─ tee-ops → console + file
61
- └─ tee-biz → kafka + slack
62
- ```
63
-
64
- Config `tee-main`:
65
- ```json
66
- {
67
- "name": "tee-main",
68
- "targets": [
69
- { "senderId": "tee", "configName": "tee-ops" },
70
- { "senderId": "tee", "configName": "tee-biz" }
71
- ]
72
- }
73
- ```
74
-
75
- ## Comportamiento
76
-
77
- - Los envíos a todos los targets se hacen en **paralelo** (`Promise.all`).
78
- - Si un target falla, el error se propaga (el `Promise.all` rechaza). Los targets completados antes del fallo **no se revierten**.
79
- - El `ISenderAccess` se inyecta en `startSender()` — el tee no funciona antes de que el sistema lo inicialice.
package/build.mjs DELETED
@@ -1,61 +0,0 @@
1
- import esbuild from 'esbuild'
2
- import fs from 'fs'
3
- import path from 'path'
4
-
5
- const kwirthGlobalsPlugin = {
6
- name: 'kwirth-globals',
7
- setup(build) {
8
- const globals = {
9
- 'react': 'window.__kwirth__.React',
10
- '@mui/material': 'window.__kwirth__.MUI.material',
11
- '@mui/icons-material': 'window.__kwirth__.MUI.icons',
12
- '@kwirthmagnify/kwirth-common': 'window.__kwirth__.kwirthCommon',
13
- }
14
- for (const pkg of Object.keys(globals)) {
15
- build.onResolve({ filter: new RegExp(`^${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) }, () => ({ path: pkg, namespace: 'kwirth-globals' }))
16
- }
17
- build.onLoad({ filter: /.*/, namespace: 'kwirth-globals' }, (args) => ({
18
- contents: `module.exports = ${globals[args.path]}`, loader: 'js',
19
- }))
20
- },
21
- }
22
-
23
- fs.mkdirSync('dist', { recursive: true })
24
-
25
- await esbuild.build({
26
- entryPoints: ['src/back/index.ts'],
27
- bundle: true,
28
- format: 'cjs',
29
- platform: 'node',
30
- target: 'node20',
31
- outfile: 'dist/back.js',
32
- loader: { '.ts': 'ts' },
33
- minify: false,
34
- })
35
- console.log('Built dist/back.js')
36
-
37
- await esbuild.build({
38
- entryPoints: ['src/front/index.tsx'],
39
- bundle: true,
40
- format: 'iife',
41
- outfile: 'dist/front.js',
42
- plugins: [kwirthGlobalsPlugin],
43
- loader: { '.tsx': 'tsx', '.ts': 'ts' },
44
- jsx: 'transform',
45
- jsxFactory: 'React.createElement',
46
- jsxFragment: 'React.Fragment',
47
- target: 'es2020',
48
- minify: false,
49
- })
50
- console.log('Built dist/front.js')
51
-
52
- const meta = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
53
- const distMeta = {
54
- id: meta.id,
55
- name: meta.name,
56
- displayName: meta.displayName,
57
- version: meta.version,
58
- description: meta.description,
59
- }
60
- fs.writeFileSync(path.join('dist', 'package.json'), JSON.stringify(distMeta, null, 2))
61
- console.log('Wrote dist/package.json')
package/dist/package.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "id": "tee",
3
- "name": "@kwirthmagnify/kwirth-sender-tee",
4
- "displayName": "Tee Sender",
5
- "version": "0.1.4",
6
- "description": "Tee sender plugin for Kwirth — fans out an incoming message to multiple downstream senders simultaneously, acting as an intermediate pipeline node"
7
- }
package/src/back/index.ts DELETED
@@ -1,68 +0,0 @@
1
- import { ISender, ISenderAccess, ISenderConfig, ISenderFieldDef, ISenderMessage } from '@kwirthmagnify/kwirth-common-back'
2
-
3
- // ─── Config ────────────────────────────────────────────────────────────────────
4
-
5
- export interface ITeeSenderTarget {
6
- senderId: string
7
- configName: string
8
- }
9
-
10
- export interface ITeeSenderConfig extends ISenderConfig {
11
- name: string
12
- targets: ITeeSenderTarget[] // list of downstream (senderId, configName) pairs to fan out to
13
- }
14
-
15
- // ─── Sender ────────────────────────────────────────────────────────────────────
16
-
17
- export class TeeSender implements ISender {
18
- readonly id = 'tee'
19
- private configs = new Map<string, ITeeSenderConfig>()
20
- private senderAccess: ISenderAccess | undefined
21
-
22
- addConfig(config: ISenderConfig): void {
23
- const tc = config as ITeeSenderConfig
24
- if (!Array.isArray(tc.targets)) tc.targets = []
25
- this.configs.set(tc.name, tc)
26
- }
27
-
28
- removeConfig(name: string): void {
29
- this.configs.delete(name)
30
- }
31
-
32
- hasConfig(name: string): boolean {
33
- return this.configs.has(name)
34
- }
35
-
36
- getConfigNames(): string[] {
37
- return Array.from(this.configs.keys())
38
- }
39
-
40
- async send(configName: string, message: ISenderMessage): Promise<void> {
41
- const config = this.configs.get(configName)
42
- if (!config) throw new Error(`TeeSender: config '${configName}' not found`)
43
- if (!this.senderAccess) throw new Error(`TeeSender: senderAccess not initialized`)
44
-
45
- await Promise.all(
46
- config.targets.map(target =>
47
- this.senderAccess!.send(target.senderId, target.configName, message)
48
- )
49
- )
50
- }
51
-
52
- getConfigSchema(): ISenderFieldDef[] {
53
- return [
54
- { name: 'name', label: 'Name', required: true },
55
- { name: 'targets', label: 'Targets (JSON)', type: 'json' },
56
- ]
57
- }
58
-
59
- async startSender(senders: ISenderAccess): Promise<void> {
60
- this.senderAccess = senders
61
- }
62
-
63
- async stopSender(): Promise<void> {
64
- this.senderAccess = undefined
65
- }
66
- }
67
-
68
- export default TeeSender
@@ -1,211 +0,0 @@
1
- import React, { useEffect, useState } from 'react'
2
- import {
3
- Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle,
4
- Divider, FormControl, IconButton, InputLabel, List, ListItem, ListItemButton,
5
- ListItemText, MenuItem, Select, Stack, TextField, Tooltip, Typography
6
- } from '@mui/material'
7
- import { Add, ContentCopy, Delete } from '@mui/icons-material'
8
-
9
- function authGet(token: string): RequestInit {
10
- return { headers: { Authorization: token ? `Bearer ${token}` : '', 'X-Kwirth-App': 'true' } }
11
- }
12
- function authPost(token: string, body: string): RequestInit {
13
- return { method: 'POST', body, headers: { Authorization: token ? `Bearer ${token}` : '', 'X-Kwirth-App': 'true', 'Content-Type': 'application/json' } }
14
- }
15
- function authDelete(token: string): RequestInit {
16
- return { method: 'DELETE', headers: { Authorization: token ? `Bearer ${token}` : '', 'X-Kwirth-App': 'true' } }
17
- }
18
-
19
- interface IProps { onClose: () => void; backendUrl: string; accessString: string }
20
- interface IConfig { name: string; description?: string; targets: { senderId: string; configName: string }[] }
21
- interface ISender { id: string; displayName?: string; configNames: string[] }
22
-
23
- const TeeSenderDialog: React.FC<IProps> = ({ onClose, backendUrl, accessString }) => {
24
- const [configs, setConfigs] = useState<IConfig[]>([])
25
- const [senders, setSenders] = useState<ISender[]>([])
26
- const [selectedName, setSelectedName] = useState<string | undefined>()
27
- const [originalName, setOriginalName] = useState<string | undefined>()
28
- const [editName, setEditName] = useState('')
29
- const [editDescription, setEditDescription] = useState('')
30
- const [targets, setTargets] = useState<{ senderId: string; configName: string }[]>([])
31
- const [newSenderId, setNewSenderId] = useState('')
32
- const [newConfigName, setNewConfigName] = useState('')
33
- const [saving, setSaving] = useState(false)
34
- const [deleting, setDeleting] = useState<string | undefined>()
35
- const [error, setError] = useState<string | undefined>()
36
-
37
- useEffect(() => { loadConfigs(); loadSenders() }, [])
38
-
39
- const loadConfigs = async () => {
40
- try {
41
- const res = await fetch(`${backendUrl}/senders/tee/configs`, authGet(accessString))
42
- if (res.ok) { const data = await res.json(); setConfigs(Array.isArray(data) ? data : (data.configs ?? [])) }
43
- } catch {}
44
- }
45
-
46
- const loadSenders = async () => {
47
- try {
48
- const res = await fetch(`${backendUrl}/senders`, authGet(accessString))
49
- if (res.ok) setSenders((await res.json() as ISender[]).filter(s => s.id !== 'tee'))
50
- } catch {}
51
- }
52
-
53
- const selectConfig = (cfg: IConfig) => {
54
- setSelectedName(cfg.name); setOriginalName(cfg.name)
55
- setEditName(cfg.name); setEditDescription(cfg.description ?? '')
56
- setTargets([...cfg.targets])
57
- setNewSenderId(''); setNewConfigName(''); setError(undefined)
58
- }
59
-
60
- const newConfig = () => {
61
- setSelectedName(undefined); setOriginalName(undefined)
62
- setEditName(''); setEditDescription(''); setTargets([])
63
- setNewSenderId(''); setNewConfigName(''); setError(undefined)
64
- }
65
-
66
- const cloneConfig = () => {
67
- if (!selectedName) return
68
- const base = configs.find(c => c.name === selectedName)
69
- if (!base) return
70
- setSelectedName(undefined); setOriginalName(undefined)
71
- setEditName(`${base.name} (copy)`); setEditDescription(base.description ?? '')
72
- setTargets([...base.targets])
73
- setNewSenderId(''); setNewConfigName(''); setError(undefined)
74
- }
75
-
76
- const addTarget = () => {
77
- if (!newSenderId || !newConfigName) return
78
- if (targets.some(t => t.senderId === newSenderId && t.configName === newConfigName)) return
79
- setTargets(prev => [...prev, { senderId: newSenderId, configName: newConfigName }])
80
- setNewSenderId(''); setNewConfigName('')
81
- }
82
-
83
- const save = async () => {
84
- const trimmed = editName.trim()
85
- if (!trimmed) return
86
- setSaving(true); setError(undefined)
87
- try {
88
- const res = await fetch(`${backendUrl}/senders/tee/configs`, authPost(accessString, JSON.stringify({ name: trimmed, ...(editDescription.trim() ? { description: editDescription.trim() } : {}), targets })))
89
- if (!res.ok) throw new Error((await res.json()).error ?? `HTTP ${res.status}`)
90
- if (originalName && originalName !== trimmed) {
91
- await fetch(`${backendUrl}/senders/tee/configs/${encodeURIComponent(originalName)}`, authDelete(accessString))
92
- }
93
- setSelectedName(trimmed); setOriginalName(trimmed)
94
- await loadConfigs()
95
- } catch (err) { setError(`Save failed: ${err}`) }
96
- finally { setSaving(false) }
97
- }
98
-
99
- const deleteConfig = async (name: string) => {
100
- setDeleting(name)
101
- try {
102
- const res = await fetch(`${backendUrl}/senders/tee/configs/${encodeURIComponent(name)}`, authDelete(accessString))
103
- if (!res.ok) throw new Error(`HTTP ${res.status}`)
104
- if (selectedName === name) newConfig()
105
- await loadConfigs()
106
- } catch (err) { setError(`Delete failed: ${err}`) }
107
- finally { setDeleting(undefined) }
108
- }
109
-
110
- const newSenderConfigs = senders.find(s => s.id === newSenderId)?.configNames ?? []
111
-
112
- return (
113
- <Dialog open={true} maxWidth={false} sx={{ '& .MuiDialog-paper': { width: '700px', height: '520px' } }}>
114
- <DialogTitle>Configure: Tee Sender</DialogTitle>
115
- <DialogContent sx={{ display: 'flex', gap: 2, p: 2, pt: 2, overflow: 'hidden', height: '100%' }}>
116
-
117
- {/* Left — config list */}
118
- <Box sx={{ width: 190, display: 'flex', flexDirection: 'column', gap: 1, flexShrink: 0 }}>
119
- <Typography variant='caption' color='text.secondary' fontWeight='bold'>Configs</Typography>
120
- <Box sx={{ flex: 1, border: 1, borderColor: 'divider', borderRadius: 1, overflowY: 'auto' }}>
121
- <List dense disablePadding>
122
- {configs.map(cfg => (
123
- <ListItem key={cfg.name} disablePadding secondaryAction={
124
- <IconButton size='small' color='error' disabled={deleting === cfg.name} onClick={() => deleteConfig(cfg.name)}>
125
- {deleting === cfg.name ? <CircularProgress size={12} /> : <Delete sx={{ fontSize: 14 }} />}
126
- </IconButton>
127
- }>
128
- <ListItemButton selected={selectedName === cfg.name} onClick={() => selectConfig(cfg)} dense sx={{ pr: 4 }}>
129
- <ListItemText
130
- primary={cfg.name}
131
- secondary={`${cfg.targets.length} target${cfg.targets.length !== 1 ? 's' : ''}`}
132
- primaryTypographyProps={{ variant: 'body2', noWrap: true }}
133
- secondaryTypographyProps={{ variant: 'caption' }} />
134
- </ListItemButton>
135
- </ListItem>
136
- ))}
137
- </List>
138
- </Box>
139
- <Stack direction='row' spacing={0.5}>
140
- <Button size='small' startIcon={<Add />} onClick={newConfig} sx={{ flex: 1 }}>New</Button>
141
- <Button size='small' startIcon={<ContentCopy />} onClick={cloneConfig} disabled={!selectedName} sx={{ flex: 1 }}>Clone</Button>
142
- </Stack>
143
- </Box>
144
-
145
- <Divider orientation='vertical' flexItem />
146
-
147
- {/* Right — editor */}
148
- <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 1.5, minWidth: 0, pt: 1 }}>
149
- <Typography variant='caption' color='text.secondary' fontWeight='bold'>
150
- {selectedName ? `Editing: ${selectedName}` : 'New config'}
151
- </Typography>
152
- <TextField size='small' label='Name *' value={editName}
153
- onChange={e => setEditName(e.target.value)} fullWidth />
154
- <TextField size='small' label='Description' value={editDescription}
155
- onChange={e => setEditDescription(e.target.value)} fullWidth multiline maxRows={2} />
156
-
157
- <Typography variant='caption' color='text.secondary' fontWeight='bold'>Targets</Typography>
158
-
159
- <Box sx={{ flex: 1, border: 1, borderColor: 'divider', borderRadius: 1, overflowY: 'auto' }}>
160
- {targets.length === 0
161
- ? <Typography variant='caption' color='text.disabled' sx={{ p: 1, display: 'block' }}>No targets yet.</Typography>
162
- : targets.map((t, i) => (
163
- <Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 1, px: 1, py: 0.5, borderBottom: 1, borderColor: 'divider' }}>
164
- <Typography variant='body2' sx={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.senderId}</Typography>
165
- <Typography variant='body2' color='text.secondary' sx={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.configName}</Typography>
166
- <IconButton size='small' color='error' onClick={() => setTargets(prev => prev.filter((_, idx) => idx !== i))}>
167
- <Delete sx={{ fontSize: 14 }} />
168
- </IconButton>
169
- </Box>
170
- ))
171
- }
172
- </Box>
173
-
174
- <Stack direction='row' spacing={1} alignItems='center'>
175
- <FormControl size='small' sx={{ flex: 1 }}>
176
- <InputLabel>Sender</InputLabel>
177
- <Select label='Sender' value={newSenderId} onChange={e => { setNewSenderId(e.target.value); setNewConfigName('') }}>
178
- {senders.map(s => <MenuItem key={s.id} value={s.id}>{s.displayName || s.id}</MenuItem>)}
179
- </Select>
180
- </FormControl>
181
- <FormControl size='small' sx={{ flex: 1 }} disabled={!newSenderId}>
182
- <InputLabel>Config</InputLabel>
183
- <Select label='Config' value={newConfigName} onChange={e => setNewConfigName(e.target.value)}>
184
- {newSenderConfigs.map(c => <MenuItem key={c} value={c}>{c}</MenuItem>)}
185
- </Select>
186
- </FormControl>
187
- <Tooltip title='Add target'>
188
- <span>
189
- <IconButton size='small' color='primary' disabled={!newSenderId || !newConfigName} onClick={addTarget}>
190
- <Add />
191
- </IconButton>
192
- </span>
193
- </Tooltip>
194
- </Stack>
195
-
196
- <Stack direction='row' justifyContent='flex-end' alignItems='center' spacing={1}>
197
- {error && <Typography variant='caption' color='error' sx={{ flex: 1 }}>{error}</Typography>}
198
- <Button size='small' variant='contained' disabled={saving || !editName.trim()} onClick={save}>
199
- {saving ? <CircularProgress size={14} /> : selectedName ? 'Update' : 'Add'}
200
- </Button>
201
- </Stack>
202
- </Box>
203
- </DialogContent>
204
- <DialogActions>
205
- <Button size='small' onClick={onClose}>Close</Button>
206
- </DialogActions>
207
- </Dialog>
208
- )
209
- }
210
-
211
- export default TeeSenderDialog
@@ -1,6 +0,0 @@
1
- import TeeSenderDialog from './TeeSenderDialog'
2
-
3
- declare global { interface Window { __kwirth_senders__: Record<string, any> } }
4
-
5
- window.__kwirth_senders__ = window.__kwirth_senders__ ?? {}
6
- window.__kwirth_senders__['tee'] = TeeSenderDialog
package/tsconfig.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "lib": ["ES2020"],
8
- "types": ["node"],
9
- "skipLibCheck": true,
10
- "esModuleInterop": true
11
- },
12
- "include": ["src"]
13
- }
package/watch.mjs DELETED
@@ -1,68 +0,0 @@
1
- import esbuild from 'esbuild'
2
- import fs from 'fs'
3
- import path from 'path'
4
-
5
- const kwirthGlobalsPlugin = {
6
- name: 'kwirth-globals',
7
- setup(build) {
8
- const globals = {
9
- 'react': 'window.__kwirth__.React',
10
- '@mui/material': 'window.__kwirth__.MUI.material',
11
- '@mui/icons-material': 'window.__kwirth__.MUI.icons',
12
- '@kwirthmagnify/kwirth-common': 'window.__kwirth__.kwirthCommon',
13
- }
14
- for (const pkg of Object.keys(globals)) {
15
- build.onResolve({ filter: new RegExp(`^${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) }, () => ({ path: pkg, namespace: 'kwirth-globals' }))
16
- }
17
- build.onLoad({ filter: /.*/, namespace: 'kwirth-globals' }, (args) => ({
18
- contents: `module.exports = ${globals[args.path]}`, loader: 'js',
19
- }))
20
- },
21
- }
22
-
23
- const notifyPlugin = (label) => ({
24
- name: `notify-${label}`,
25
- setup(build) {
26
- build.onEnd(result => {
27
- const t = new Date().toLocaleTimeString()
28
- if (result.errors.length) {
29
- console.error(`[${t}] ${label} FAILED: ${result.errors.map(e => e.text).join('; ')}`)
30
- } else {
31
- console.log(`[${t}] ${label} OK`)
32
- }
33
- })
34
- },
35
- })
36
-
37
- fs.mkdirSync('dist', { recursive: true })
38
- const meta = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
39
- fs.writeFileSync(path.join('dist', 'package.json'), JSON.stringify({ id: meta.id, name: meta.name, displayName: meta.displayName, version: meta.version, description: meta.description }, null, 2))
40
-
41
- const backCtx = await esbuild.context({
42
- entryPoints: ['src/back/index.ts'],
43
- bundle: true,
44
- format: 'cjs',
45
- platform: 'node',
46
- target: 'node20',
47
- outfile: 'dist/back.js',
48
- loader: { '.ts': 'ts' },
49
- minify: false,
50
- plugins: [notifyPlugin('back')],
51
- })
52
-
53
- const frontCtx = await esbuild.context({
54
- entryPoints: ['src/front/index.tsx'],
55
- bundle: true,
56
- format: 'iife',
57
- outfile: 'dist/front.js',
58
- plugins: [kwirthGlobalsPlugin, notifyPlugin('front')],
59
- loader: { '.tsx': 'tsx', '.ts': 'ts' },
60
- jsx: 'transform',
61
- jsxFactory: 'React.createElement',
62
- jsxFragment: 'React.Fragment',
63
- target: 'es2020',
64
- minify: false,
65
- })
66
-
67
- await Promise.all([backCtx.watch(), frontCtx.watch()])
68
- console.log('[watch] Watching src/ — back.js and front.js rebuild on every change.')
File without changes