@tecsom/apipos-js 0.0.0-stage → 2.0.3
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/LICENSE +21 -0
- package/README.md +398 -2
- package/dist/apipos.d.ts +67 -0
- package/dist/apipos.js +291 -0
- package/dist/http.d.ts +36 -0
- package/dist/http.js +113 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/printer.d.ts +61 -0
- package/dist/printer.js +189 -0
- package/dist/strip-accents.d.ts +21 -0
- package/dist/strip-accents.js +57 -0
- package/dist/ticket-preview.d.ts +33 -0
- package/dist/ticket-preview.js +115 -0
- package/dist/types.d.ts +506 -0
- package/dist/types.js +14 -0
- package/dist/version-check.d.ts +22 -0
- package/dist/version-check.js +72 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/package.json +56 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tecsom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,399 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tecsom/apipos-js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 📖 Documentación: https://tecsom.github.io/Apipos/
|
|
4
|
+
|
|
5
|
+
Cliente oficial para [Apipos](https://github.com/Tecsom/Apipos), el agente local
|
|
6
|
+
de impresión de Tecsom que corre en cada caja (`http://127.0.0.1:50432`) y
|
|
7
|
+
expone por HTTP las tiqueteras y etiqueteras de la máquina.
|
|
8
|
+
|
|
9
|
+
- Escrita en **TypeScript estricto**; se distribuye compilada a `dist/`
|
|
10
|
+
(módulos ES + `.d.ts`). El navegador carga `dist/` tal cual, sin empaquetador.
|
|
11
|
+
- **Sin dependencias** en runtime (solo `fetch`, `Blob`, `btoa` y `atob`, que
|
|
12
|
+
existen en el navegador y en Node ≥ 18).
|
|
13
|
+
- **Desacoplada** del POS que la usa: lo que cada app quiera hacerle al texto
|
|
14
|
+
antes de imprimir entra por el hook `transformContent`.
|
|
15
|
+
- **Versionada en lockstep con Apipos**: la librería `X.Y.Z` está hecha para
|
|
16
|
+
Apipos `X.Y.Z` (ver [Versionado](#versionado-en-lockstep-con-apipos)).
|
|
17
|
+
|
|
18
|
+
El contrato HTTP que envuelve está en `APIPOS_API.md` del repo de Apipos.
|
|
19
|
+
|
|
20
|
+
## Instalación
|
|
21
|
+
|
|
22
|
+
| Caso | Comando |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Desarrollo local (repos hermanos en `REPOSITORIES/`) | `pnpm add -D "file:../apipos-js"` |
|
|
25
|
+
| Release, desde git | `pnpm add -D "github:Tecsom/apipos-js#vX.Y.Z"` |
|
|
26
|
+
| Release, tarball | `pnpm add -D "https://github.com/Tecsom/apipos-js/releases/download/vX.Y.Z/tecsom-apipos-js-X.Y.Z.tgz"` |
|
|
27
|
+
|
|
28
|
+
Desde git, el gestor de paquetes instala las devDependencies y corre `prepare`
|
|
29
|
+
(= `npm run build`), así que `dist/` se genera solo. El tarball ya trae `dist/`.
|
|
30
|
+
|
|
31
|
+
Usa siempre la versión **igual** a la del Apipos instalado en las cajas.
|
|
32
|
+
|
|
33
|
+
Para servirla al navegador sin empaquetador, copia
|
|
34
|
+
`node_modules/@tecsom/apipos-js/dist/` a una carpeta estática e impórtala desde
|
|
35
|
+
ahí (`.../apipos-js/index.js`).
|
|
36
|
+
|
|
37
|
+
## Uso
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Apipos, ApiposError } from '@tecsom/apipos-js';
|
|
41
|
+
import type { TicketContent } from '@tecsom/apipos-js';
|
|
42
|
+
|
|
43
|
+
const apipos = new Apipos(); // baseUrl por defecto: http://127.0.0.1:50432
|
|
44
|
+
|
|
45
|
+
// ¿Está corriendo Apipos? Nunca lanza.
|
|
46
|
+
const { ok, data, version } = await apipos.checkHealth();
|
|
47
|
+
// data: { status, version, os, platform, backend, port, client }
|
|
48
|
+
// version: { client: '1.0.6', backend: '1.0.6', status: 'equal', message: null } (null si ok es false)
|
|
49
|
+
|
|
50
|
+
// Nombres EXACTOS de las impresoras instaladas ([] si Apipos no responde).
|
|
51
|
+
const printers = await apipos.listPrinters(); // ['POS80_Kinwodon', 'AiYin_AS240_BT']
|
|
52
|
+
|
|
53
|
+
// Impresora: nombre exacto + settings. `paper_size` numérico es obligatorio
|
|
54
|
+
// (lanza `Missing required setting: paper_size` /
|
|
55
|
+
// `Invalid type for setting paper_size: expected number, got <tipo>`).
|
|
56
|
+
// Sin nombre devuelve null (y lo reporta en consola).
|
|
57
|
+
const printer = apipos.createPrinter('POS80_Kinwodon', { paper_size: 80 });
|
|
58
|
+
|
|
59
|
+
// Ticket RAW (ESC/POS).
|
|
60
|
+
const ticket: TicketContent = [
|
|
61
|
+
{ type: 'text', data: 'MI TIENDA S.A.', align: 'center', font_size: 'md', font_weight: 'bold' },
|
|
62
|
+
{ type: 'separator' },
|
|
63
|
+
{ type: 'table', data: { rows: [[2, 'Coca Cola 600ml', '$18.00', '$36.00']] } },
|
|
64
|
+
{ type: 'special_text', data: { text1: 'TOTAL', text2: '$36.00' }, font_weight: 'bold' },
|
|
65
|
+
{ type: 'open_withdrawer' }
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await printer?.print(ticket);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof ApiposError) {
|
|
72
|
+
// Mensaje de Apipos (p. ej. "Printer 'X' not found.") y su `data`
|
|
73
|
+
// (p. ej. { available: [...] } con las impresoras válidas).
|
|
74
|
+
console.error(error.message, error.data);
|
|
75
|
+
} else {
|
|
76
|
+
// Sin conexión: "No se pudo conectar con Apipos (<url>). ¿Está abierta la app de bandeja?"
|
|
77
|
+
console.error(error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// PDF (Blob): 'driver' para etiqueteras con driver (AiYin), 'raw' para tiqueteras.
|
|
82
|
+
await printer?.printPDF(pdfBlob, { pdfMode: 'driver', labelSettings: { width: 60, height: 30, unit: 'mm' } });
|
|
83
|
+
|
|
84
|
+
// Abrir el cajón de dinero sin imprimir (POST /open-withdrawer { printer }).
|
|
85
|
+
await printer?.openDrawer(); // → { printer: 'POS80_Kinwodon' }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `print(content)` manda exactamente `{ printer, type: 'RAW', settings, content }`
|
|
89
|
+
a `POST /print`; `printPDF(blob, options)` manda `{ printer, type: 'PDF', settings, data }`
|
|
90
|
+
con el PDF en base64. Ambos devuelven el `data` de la respuesta de Apipos.
|
|
91
|
+
- `print(content, options)` acepta opciones del renderizador PDF (siguiente
|
|
92
|
+
sección); sin `options` (o con `{}`) el payload es el de siempre, byte a byte.
|
|
93
|
+
- `apipos.apiUrl` (get/set) se conserva como alias de `baseUrl` por compatibilidad.
|
|
94
|
+
- `new Apipos({ baseUrl, fetch, transformContent, onVersionMismatch })`: `fetch`
|
|
95
|
+
es inyectable (pruebas); por defecto se usa el `fetch` global vigente en cada
|
|
96
|
+
petición. `onVersionMismatch` reemplaza el aviso de versión en consola (ver
|
|
97
|
+
[Versionado](#versionado-en-lockstep-con-apipos)).
|
|
98
|
+
|
|
99
|
+
Toda petición lleva el header `X-Apipos-Client: apipos-js/<versión>` y toda
|
|
100
|
+
respuesta de Apipos trae `X-Apipos-Version`: así cada lado sabe la versión del
|
|
101
|
+
otro (handshake). Si no coinciden, la librería avisa en la consola del navegador.
|
|
102
|
+
|
|
103
|
+
## `transformContent`: preparar el texto antes de imprimir
|
|
104
|
+
|
|
105
|
+
La librería no toca el texto del ticket. Si tu app necesita limpiarlo (comillas
|
|
106
|
+
tipográficas, quitar acentos para tiqueteras sin Latin-1...), pásalo como hook:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { Apipos, stripAccentsFromContent } from '@tecsom/apipos-js';
|
|
110
|
+
|
|
111
|
+
const apipos = new Apipos({
|
|
112
|
+
// Se llama en CADA trabajo (un ajuste cambiado vale desde el siguiente ticket)
|
|
113
|
+
// y debe devolver un arreglo NUEVO: la librería nunca muta lo que recibe.
|
|
114
|
+
transformContent: content => (deviceWantsNoAccents() ? stripAccentsFromContent(content) : content.slice())
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
El default es una copia superficial (`content.slice()`). `stripAccents` y
|
|
119
|
+
`stripAccentsFromContent` (también en `@tecsom/apipos-js/strip-accents`) quitan
|
|
120
|
+
acentos, `ñ`, `¿ ¡` y ordinales sin mover el ancho de las líneas, y no recorren
|
|
121
|
+
el base64 de las imágenes.
|
|
122
|
+
|
|
123
|
+
## Imprimir renderizando en Apipos (`renderer: 'pdf'`)
|
|
124
|
+
|
|
125
|
+
Con `renderer: 'pdf'` Apipos maqueta el MISMO `content` como un PDF de una sola
|
|
126
|
+
página (del alto exacto del ticket) con fuentes del sistema o integradas, y lo
|
|
127
|
+
imprime por la ruta PDF (`pdfMode: 'raw'` rasteriza a ESC/POS para tiqueteras;
|
|
128
|
+
`'driver'` lo entrega al driver del SO). Así hay tipografías proporcionales,
|
|
129
|
+
tamaños en puntos, cursiva, subrayado, columnas de tabla a medida, bloques
|
|
130
|
+
`html` y separadores con estilo. Sin `renderer` (o con `'escpos'`) el ticket
|
|
131
|
+
sale por ESC/POS como siempre y los campos del renderizador PDF se ignoran.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { Apipos } from '@tecsom/apipos-js';
|
|
135
|
+
import type { PrintOptions, TicketContent } from '@tecsom/apipos-js';
|
|
136
|
+
|
|
137
|
+
const apipos = new Apipos();
|
|
138
|
+
const printer = apipos.createPrinter('POS80_Kinwodon', { paper_size: 80 });
|
|
139
|
+
|
|
140
|
+
const ticket: TicketContent = [
|
|
141
|
+
{ type: 'image', data: logoBase64, width_mm: 30, align: 'center' },
|
|
142
|
+
{ type: 'text', data: 'MI TIENDA S.A.', align: 'center', font_size: 18, font_weight: 'bold' },
|
|
143
|
+
{ type: 'text', data: 'PEDIDO #42', align: 'center', high_contrast: true },
|
|
144
|
+
{ type: 'separator', style: 'dashed' },
|
|
145
|
+
{
|
|
146
|
+
type: 'table',
|
|
147
|
+
data: {
|
|
148
|
+
columns: [
|
|
149
|
+
{ label: 'Cant.', width: 15, align: 'left' },
|
|
150
|
+
{ label: 'Producto', width: 45 },
|
|
151
|
+
{ label: 'Precio', width: 20, align: 'right' },
|
|
152
|
+
{ label: 'Importe', width: 20, align: 'right' }
|
|
153
|
+
],
|
|
154
|
+
rows: [
|
|
155
|
+
[2, 'Coca Cola 600ml', '$18.00', '$36.00'],
|
|
156
|
+
[1, 'Pan Bimbo', '$45.50', '$45.50']
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{ type: 'separator', style: 'solid', thickness: 1 },
|
|
161
|
+
{ type: 'special_text', data: { text1: 'TOTAL', text2: '$81.50' }, font_size: 12, font_weight: 'bold' },
|
|
162
|
+
{ type: 'special_text', data: { text1: 'Folio', text2: '000123' }, font_family: 'Courier New' },
|
|
163
|
+
{ type: 'spacer', lines: 1 },
|
|
164
|
+
{ type: 'text', data: 'Términos en www.mitienda.mx', font_size: 7, font_style: 'italic', underline: true },
|
|
165
|
+
{ type: 'spacer', height_mm: 4 },
|
|
166
|
+
{ type: 'html', data: '<p style="text-align:center">Gracias por su <b>compra</b></p>' },
|
|
167
|
+
{ type: 'open_withdrawer' }
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const options: PrintOptions = {
|
|
171
|
+
renderer: 'pdf',
|
|
172
|
+
pdfMode: 'raw', // tiquetera; 'driver' para etiqueteras con driver
|
|
173
|
+
font: { family: 'Arial', size: 9, line_height: 1.25 }, // una familia de listFonts() o 'sans-serif' | 'serif' | 'monospace'
|
|
174
|
+
marginsMm: { top: 1, right: 1, bottom: 3, left: 1 },
|
|
175
|
+
cut: true
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = await printer?.print(ticket, options);
|
|
179
|
+
// result: { printer, type: 'RAW', renderer: 'pdf', strategy: 'raw', width_dots: 576, cut: true,
|
|
180
|
+
// page_mm: { width: 72, height: 123.4 }, fonts: [...], warnings: [...], drawer_opened: true }
|
|
181
|
+
console.warn(result?.warnings); // p. ej. "Font family 'X' is not installed; using sans-serif."
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Las opciones se copian a `settings` (nunca al nivel raíz del payload), encima de
|
|
185
|
+
los `settings` de la impresora, que no se modifican:
|
|
186
|
+
|
|
187
|
+
| Opción | Clave en `settings` |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `renderer` | `renderer` (`'escpos'` \| `'pdf'`) |
|
|
190
|
+
| `pdfMode` | `pdf_mode` |
|
|
191
|
+
| `font` | `font` (`{ family, size, line_height }`) |
|
|
192
|
+
| `marginsMm` | `margins_mm` |
|
|
193
|
+
| `widthMm` | `width_mm` |
|
|
194
|
+
| `maxHeightMm` | `max_height_mm` |
|
|
195
|
+
| `cut` | `cut` |
|
|
196
|
+
| `settings` | se mezcla al final y gana (escape para claves nuevas del backend) |
|
|
197
|
+
|
|
198
|
+
- Solo viajan las opciones definidas: `undefined` no viaja (`false` y `0` sí).
|
|
199
|
+
- La mezcla es superficial: `font` o `marginsMm` reemplazan completo el objeto
|
|
200
|
+
que traiga la impresora.
|
|
201
|
+
- `open_withdrawer` con `renderer: 'pdf'`: Apipos manda el pulso del cajón como
|
|
202
|
+
un segundo trabajo raw tras imprimir el PDF (`drawer_opened: true`).
|
|
203
|
+
|
|
204
|
+
## Fuentes del sistema
|
|
205
|
+
|
|
206
|
+
`apipos.listFonts()` → `GET /fonts`: las familias instaladas en la máquina de
|
|
207
|
+
Apipos, para elegir `font.family` o el `font_family` de un item. Lanza si
|
|
208
|
+
Apipos no responde (a diferencia de `listPrinters`, que devuelve `[]`).
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { Apipos } from '@tecsom/apipos-js';
|
|
212
|
+
import type { FontCatalog } from '@tecsom/apipos-js';
|
|
213
|
+
|
|
214
|
+
const apipos = new Apipos();
|
|
215
|
+
|
|
216
|
+
async function fillFontSelect(select: HTMLSelectElement, { refresh = false } = {}): Promise<void> {
|
|
217
|
+
// refresh: true → GET /fonts?refresh=1 (vuelve a escanear, p. ej. tras instalar una fuente).
|
|
218
|
+
const { families, builtin, scannedAt }: FontCatalog = await apipos.listFonts({ refresh });
|
|
219
|
+
// `builtin` (sans-serif, serif, monospace) existe siempre, aunque la máquina no tenga fuentes.
|
|
220
|
+
const names = [...builtin, ...families.map(family => family.family)];
|
|
221
|
+
select.replaceChildren(...names.map(name => new Option(name, name)));
|
|
222
|
+
select.title = `Fuentes escaneadas: ${scannedAt}`;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`families` viene ordenada por nombre; cada familia trae `monospace` y sus caras
|
|
227
|
+
(`faces`: `style`, `bold`, `italic`, `weight`, `full_name`, `path`, `format`...).
|
|
228
|
+
También devuelve `count` (número de familias) y `dirs` (directorios escaneados).
|
|
229
|
+
|
|
230
|
+
## Vista previa
|
|
231
|
+
|
|
232
|
+
### Real (la pinta Apipos)
|
|
233
|
+
|
|
234
|
+
`POST /render` maqueta el ticket con el MISMO renderizador PDF que imprime y lo
|
|
235
|
+
devuelve sin imprimir (no valida ni toca impresoras). `content` pasa por
|
|
236
|
+
`transformContent`, igual que al imprimir.
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { Apipos, renderTicketPreviewPng } from '@tecsom/apipos-js';
|
|
240
|
+
|
|
241
|
+
const apipos = new Apipos();
|
|
242
|
+
|
|
243
|
+
// data URL listo para un <img> (png a 150 dpi por defecto).
|
|
244
|
+
const img = document.querySelector<HTMLImageElement>('#ticket-preview');
|
|
245
|
+
if (img) {
|
|
246
|
+
img.src = await renderTicketPreviewPng(apipos, ticket, {
|
|
247
|
+
settings: { paper_size: 80, font: { family: 'Arial', size: 9 } },
|
|
248
|
+
dpi: 150
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Con los settings de UNA impresora y las mismas opciones que print() (png a 203 dpi por defecto).
|
|
253
|
+
const preview = await printer?.render(ticket, { font: { family: 'Arial', size: 9 } });
|
|
254
|
+
|
|
255
|
+
// O el resultado completo: { format, dpi, base64, pageMm, pages, fonts, warnings, blob }.
|
|
256
|
+
const pdf = await apipos.renderTicket(ticket, { settings: { paper_size: 80 }, format: 'pdf' });
|
|
257
|
+
window.open(URL.createObjectURL(pdf.blob)); // blob: application/pdf (image/png con format: 'png')
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
- `renderTicket(content, { settings = {}, format = 'pdf', dpi = 203 })`; `dpi`
|
|
261
|
+
(72–600) solo aplica con `png` y en `pdf` el resultado trae `dpi: null`.
|
|
262
|
+
- `base64ToBlob(base64, mime)` (base64 puro o data URI) es la misma conversión
|
|
263
|
+
que arma `blob`, por si guardas el `base64` y lo necesitas como archivo.
|
|
264
|
+
|
|
265
|
+
### Aproximación en pantalla (ESC/POS, sin Apipos)
|
|
266
|
+
|
|
267
|
+
`@tecsom/apipos-js/ticket-preview` exporta `renderTicketHtml(content, { paperSize })`
|
|
268
|
+
(HTML monoespaciado con las columnas del papel: 48 en 80 mm, 32 en 58 mm),
|
|
269
|
+
`TICKET_PREVIEW_CSS`, `ticketColumns(paperSize)` y `printTicketHtml(html, { title })`
|
|
270
|
+
(imprime desde el navegador en un iframe oculto). No necesita a Apipos, pero es
|
|
271
|
+
una aproximación del camino ESC/POS; `renderTicketPreviewPng` (también en ese
|
|
272
|
+
subpath) es la vista previa real.
|
|
273
|
+
|
|
274
|
+
## Tipos
|
|
275
|
+
|
|
276
|
+
Todos los tipos públicos se exportan desde `@tecsom/apipos-js` (y desde
|
|
277
|
+
`@tecsom/apipos-js/types`). Los más usados:
|
|
278
|
+
|
|
279
|
+
- `TicketContent` / `TicketItem`: unión discriminada por `type` (`text`,
|
|
280
|
+
`special_text`, `table`, `separator`, `image`, `spacer`, `html`,
|
|
281
|
+
`open_withdrawer`) con todos los campos de `APIPOS_API.md`, incluidos los del
|
|
282
|
+
renderizador PDF (`font_family`, `font_size` en puntos, `underline`, columnas
|
|
283
|
+
de tabla...). Un `type` o un valor fuera del contrato no compila.
|
|
284
|
+
- `PrintSettings`: los `settings` en el formato del cable (`paper_size`,
|
|
285
|
+
`char_width`, `renderer`, `pdf_mode`, `font`, `margins_mm`, `label_settings`...).
|
|
286
|
+
- `PrintOptions` (de `print`), `PrinterRenderOptions` (de `printer.render`),
|
|
287
|
+
`RenderTicketOptions`, `RenderPreviewOptions`, `ListFontsOptions`.
|
|
288
|
+
- `FontCatalog` / `FontFamily` / `FontFace`, `RenderResult` / `RenderedFont`,
|
|
289
|
+
`OpenDrawerResult`, `HealthResult`, `HealthData`, `PrintResult`,
|
|
290
|
+
`VersionInfo` / `VersionStatus` / `VersionTuple`, `ApiposOptions`, `FetchLike`...
|
|
291
|
+
|
|
292
|
+
Los items y `settings` van en snake_case porque viajan tal cual a Apipos; las
|
|
293
|
+
opciones propias de la librería y los resultados ya adaptados van en camelCase
|
|
294
|
+
(`pdfMode`, `marginsMm`, `labelSettings`, `scannedAt`, `pageMm`).
|
|
295
|
+
|
|
296
|
+
## Versionado en lockstep con Apipos
|
|
297
|
+
|
|
298
|
+
La versión de esta librería **siempre** es el `APP_VERSION` de
|
|
299
|
+
`Apipos/app-meta.env`; nunca se edita a mano aquí. La sube el usuario en Apipos y
|
|
300
|
+
se propaga con:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
npm run sync-version # app-meta.env → package.json + src/version.ts
|
|
304
|
+
npm run check-version # exit 1 si alguna difiere (antes de un release)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Por defecto leen `../Apipos/app-meta.env` (el checkout hermano); con otra ruta:
|
|
308
|
+
`node scripts/sync-version.ts --check --from <ruta>`.
|
|
309
|
+
|
|
310
|
+
Un release publica el mismo tag `vX.Y.Z` en ambos repos: Apipos con sus
|
|
311
|
+
instaladores y apipos-js con `release/tecsom-apipos-js-X.Y.Z.tgz` (`npm run pack`).
|
|
312
|
+
El checklist completo está en el README de Apipos ("Librería JS (`apipos-js`)").
|
|
313
|
+
|
|
314
|
+
### Handshake: el aviso en la consola
|
|
315
|
+
|
|
316
|
+
La librería manda `X-Apipos-Client: apipos-js/<VERSION>` en cada petición y lee
|
|
317
|
+
el header `X-Apipos-Version` de cada respuesta de Apipos (también de las de
|
|
318
|
+
error). Si las versiones no coinciden, avisa con `console.warn`. El aviso es
|
|
319
|
+
informativo: **nunca bloquea ni cancela la petición**, el ticket se imprime igual.
|
|
320
|
+
|
|
321
|
+
| Aviso en la consola | Qué pasa | Cómo se resuelve |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| `[apipos-js] Apipos X.Y.Z es más nuevo que la librería apipos-js A.B.C. Actualiza la librería en el front: ambas deben ir en la misma versión.` | El front trae una librería más vieja que el Apipos de la caja. | Actualiza la dependencia del front a `vX.Y.Z` (`pnpm add -D "github:Tecsom/apipos-js#vX.Y.Z"` o el tarball del release) y, si la sirves sin empaquetador, vuelve a copiar `dist/` a tu carpeta estática. |
|
|
324
|
+
| `[apipos-js] La librería apipos-js A.B.C es más nueva que Apipos X.Y.Z instalado en esta máquina. Actualiza Apipos (menú de bandeja → Buscar actualizaciones): ambas deben ir en la misma versión.` | La caja tiene un Apipos más viejo que la librería del front. | Actualiza Apipos en esa máquina: menú de bandeja → "Buscar actualizaciones", o el instalador del release `vA.B.C`. |
|
|
325
|
+
| `[apipos-js] Apipos no reporta su versión (falta el header X-Apipos-Version): es anterior a la librería apipos-js A.B.C. Actualiza Apipos en esta máquina.` | El Apipos de la caja es anterior a 1.0.6 (aún no tenía handshake). | Actualiza Apipos en esa máquina, igual que el caso anterior. |
|
|
326
|
+
|
|
327
|
+
- El aviso sale **una vez por página (por versión distinta de Apipos)**, no en
|
|
328
|
+
cada petición, aunque la app cree varios `new Apipos()` (por ejemplo,
|
|
329
|
+
uno por impresión). Con `onVersionMismatch`, una vez por cliente.
|
|
330
|
+
- Las respuestas sin `headers` (dobles de `fetch` en pruebas) no se evalúan.
|
|
331
|
+
- Con versiones iguales no hay ningún aviso.
|
|
332
|
+
|
|
333
|
+
`onVersionMismatch` reemplaza al `console.warn` (por ejemplo, para mostrarlo en
|
|
334
|
+
la UI o mandarlo a tu telemetría). Recibe `{ client, backend, status, message }`
|
|
335
|
+
una vez por versión distinta de Apipos **en ese cliente** (quien inyecta el hook
|
|
336
|
+
controla su propia política); si lanza, el error va a `console.error` y la
|
|
337
|
+
petición sigue:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { Apipos } from '@tecsom/apipos-js';
|
|
341
|
+
import type { VersionInfo } from '@tecsom/apipos-js';
|
|
342
|
+
|
|
343
|
+
const apipos = new Apipos({
|
|
344
|
+
onVersionMismatch: (info: VersionInfo) => {
|
|
345
|
+
// info.status: 'client_older' | 'client_newer' | 'unknown'
|
|
346
|
+
showBanner(info.message);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`checkHealth()` devuelve la comparación en `version`, calculada con
|
|
352
|
+
`data.version` de `GET /health` (útil en una pantalla de diagnóstico):
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
const { ok, version } = await apipos.checkHealth();
|
|
356
|
+
if (ok && version?.status !== 'equal') {
|
|
357
|
+
console.info(version?.message);
|
|
358
|
+
}
|
|
359
|
+
// version: { client, backend, status, message }; null cuando ok es false (sin conexión o error).
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`checkHealth()` usa el mismo aviso deduplicado: si el header ya avisó esa
|
|
363
|
+
versión no se repite, y si la respuesta no trajo headers el aviso sale de
|
|
364
|
+
`data.version`.
|
|
365
|
+
|
|
366
|
+
Las funciones puras del handshake también se exportan:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { compareVersions, describeVersions, parseVersion, VERSION } from '@tecsom/apipos-js';
|
|
370
|
+
|
|
371
|
+
parseVersion('v1.2'); // [1, 2, 0] (vacío, null o no numérico → null)
|
|
372
|
+
compareVersions('1.0.9', '1.0.10'); // 'client_older' (numérico, no de texto)
|
|
373
|
+
compareVersions(VERSION, null); // 'unknown'
|
|
374
|
+
describeVersions('1.0.6', '1.0.6'); // { client: '1.0.6', backend: '1.0.6', status: 'equal', message: null }
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Desarrollo
|
|
378
|
+
|
|
379
|
+
Requiere Node ≥ 22.18 (corre las pruebas y los scripts `.ts` directamente, por
|
|
380
|
+
type stripping).
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
npm install # instala TypeScript y @types/node (versiones exactas) y compila (prepare)
|
|
384
|
+
npm run build # tsc → dist/ (.js + .d.ts)
|
|
385
|
+
npm test # build + typecheck de pruebas + node --test (las pruebas importan dist/)
|
|
386
|
+
npm run typecheck # tsc sobre src/ y sobre test/ + scripts/
|
|
387
|
+
npm run pack # release/tecsom-apipos-js-<versión>.tgz
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Reglas del código (`src/`, `test/`, `scripts/`):
|
|
391
|
+
|
|
392
|
+
- **Sin `any`** en ninguna forma, sin `@ts-ignore` / `@ts-nocheck` /
|
|
393
|
+
`@ts-expect-error`, sin dobles aserciones y sin el operador `!` de no-nulo.
|
|
394
|
+
Lo que llega de la red entra como `unknown` y se valida con guardas de tipo;
|
|
395
|
+
si la forma no cuadra se lanza `Respuesta inesperada de Apipos en <ruta>`.
|
|
396
|
+
- Los imports relativos llevan extensión `.js` (`import { request } from './http.js'`)
|
|
397
|
+
para que el navegador cargue `dist/` sin empaquetador.
|
|
398
|
+
- `test/fixtures/invalid-usage.ts` es uso inválido a propósito: una prueba
|
|
399
|
+
compila ese archivo y exige que `tsc` lo rechace.
|
package/dist/apipos.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ApiPosPrinter } from './printer.js';
|
|
2
|
+
import type { ApiposOptions, FontCatalog, HealthResult, HttpMethod, ListFontsOptions, PrintSettings, RenderResult, RenderTicketOptions, RequestOptions, TicketContent } from './types.js';
|
|
3
|
+
export declare class Apipos {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(options?: ApiposOptions);
|
|
6
|
+
/** URL base de Apipos (default `http://127.0.0.1:50432`). */
|
|
7
|
+
get baseUrl(): string;
|
|
8
|
+
/** Alias de `baseUrl` (compatibilidad con v1). */
|
|
9
|
+
get apiUrl(): string;
|
|
10
|
+
/** Compatibilidad con v1: cambia la URL base para las siguientes peticiones. */
|
|
11
|
+
set apiUrl(url: string);
|
|
12
|
+
/**
|
|
13
|
+
* Verifica que el servicio de impresión esté disponible.
|
|
14
|
+
* GET /health → { data: { status, version, os, platform, backend, port, client }, message, status }
|
|
15
|
+
*
|
|
16
|
+
* Devuelve `{ ok, data, version }`: `ok` es `true` si el servicio responde
|
|
17
|
+
* "ok" y `version` = `describeVersions(VERSION, data.version)`
|
|
18
|
+
* (`{ client, backend, status, message }`).
|
|
19
|
+
*
|
|
20
|
+
* `data.version` pasa por el MISMO aviso deduplicado que el header: si el
|
|
21
|
+
* header ya avisó esa versión no se repite, y si la respuesta no trajo
|
|
22
|
+
* `headers` (dobles de `fetch`) el aviso sale de aquí.
|
|
23
|
+
*
|
|
24
|
+
* Nunca lanza: ante cualquier error devuelve `{ ok: false, data: null, version: null }`.
|
|
25
|
+
*/
|
|
26
|
+
checkHealth(): Promise<HealthResult>;
|
|
27
|
+
/** GET /printers → nombres exactos de las impresoras instaladas; `[]` si falla. */
|
|
28
|
+
listPrinters(): Promise<string[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Fuentes instaladas en la máquina de Apipos, para elegir `settings.font.family`
|
|
31
|
+
* o `font_family` del renderizador PDF.
|
|
32
|
+
* GET /fonts (con `refresh: true` → GET /fonts?refresh=1, que vuelve a
|
|
33
|
+
* escanear; útil tras instalar una fuente).
|
|
34
|
+
*
|
|
35
|
+
* Devuelve `{ families, builtin, count, dirs, scannedAt }`: `builtin`
|
|
36
|
+
* (`sans-serif`, `serif`, `monospace`) existe siempre, aunque la máquina no
|
|
37
|
+
* tenga fuentes. A diferencia de `listPrinters`, LANZA ante cualquier error
|
|
38
|
+
* (`ApiposError`, sin conexión o `Respuesta inesperada de Apipos en /fonts`).
|
|
39
|
+
*/
|
|
40
|
+
listFonts({ refresh }?: ListFontsOptions): Promise<FontCatalog>;
|
|
41
|
+
/**
|
|
42
|
+
* Vista previa REAL de un ticket: Apipos lo maqueta con el mismo renderizador
|
|
43
|
+
* PDF que imprime (`settings.renderer: 'pdf'`), sin validar ni tocar ninguna
|
|
44
|
+
* impresora. POST /render con `{ settings, content, format, dpi }`.
|
|
45
|
+
*
|
|
46
|
+
* `content` pasa por `transformContent`, igual que al imprimir: lo que se ve
|
|
47
|
+
* es lo que saldría en papel. `dpi` solo aplica con `png` (72–600).
|
|
48
|
+
*
|
|
49
|
+
* Devuelve `{ format, dpi, base64, pageMm, pages, fonts, warnings, blob }`;
|
|
50
|
+
* `blob` es un `Blob` `application/pdf` o `image/png` listo para
|
|
51
|
+
* `URL.createObjectURL`, y `dpi` es `null` con `pdf`. Lanza ante cualquier error.
|
|
52
|
+
*/
|
|
53
|
+
renderTicket(content: TicketContent, { settings, format, dpi }?: RenderTicketOptions): Promise<RenderResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Impresora con nombre exacto (de `listPrinters()`) y sus `settings`
|
|
56
|
+
* (`paper_size` numérico obligatorio). Sin nombre devuelve `null` y lo
|
|
57
|
+
* reporta en consola, como v1.
|
|
58
|
+
*/
|
|
59
|
+
createPrinter(name: string, settings?: PrintSettings): ApiPosPrinter | null;
|
|
60
|
+
/**
|
|
61
|
+
* Acceso a la red para `ApiPosPrinter` (y para endpoints que la librería aún
|
|
62
|
+
* no envuelve): devuelve el `data` del sobre o lanza `ApiposError`.
|
|
63
|
+
*/
|
|
64
|
+
request(method: HttpMethod, path: string, options?: RequestOptions): Promise<unknown>;
|
|
65
|
+
/** Pasa el `content` de un trabajo por `transformContent`. */
|
|
66
|
+
applyTransform(content: TicketContent): TicketContent;
|
|
67
|
+
}
|