@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 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
- # Temporary Holding Version
1
+ # @tecsom/apipos-js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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
+ }