@ressjs/platform 0.6.0-experimental.0
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 +95 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +59 -0
- package/dist/compat.d.ts +9 -0
- package/dist/compat.js +13 -0
- package/dist/detect/layers.d.ts +40 -0
- package/dist/detect/layers.js +34 -0
- package/dist/detect/merge.d.ts +30 -0
- package/dist/detect/merge.js +50 -0
- package/dist/detect/override.d.ts +9 -0
- package/dist/detect/override.js +33 -0
- package/dist/detect/pipeline.d.ts +10 -0
- package/dist/detect/pipeline.js +75 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +51 -0
- package/dist/model/defaults.d.ts +8 -0
- package/dist/model/defaults.js +25 -0
- package/dist/model/known.d.ts +31 -0
- package/dist/model/known.js +9 -0
- package/dist/model/types.d.ts +95 -0
- package/dist/model/types.js +2 -0
- package/dist/predicates.d.ts +9 -0
- package/dist/predicates.js +15 -0
- package/dist/registry/create.d.ts +17 -0
- package/dist/registry/create.js +87 -0
- package/dist/registry/defaults.d.ts +23 -0
- package/dist/registry/defaults.js +63 -0
- package/dist/registry/index.d.ts +11 -0
- package/dist/registry/index.js +20 -0
- package/dist/registry/parse.d.ts +14 -0
- package/dist/registry/parse.js +89 -0
- package/dist/registry/types.d.ts +91 -0
- package/dist/registry/types.js +2 -0
- package/dist/rules/apply.d.ts +13 -0
- package/dist/rules/apply.js +40 -0
- package/dist/rules/edge.d.ts +44 -0
- package/dist/rules/edge.js +66 -0
- package/dist/rules/headers.d.ts +48 -0
- package/dist/rules/headers.js +157 -0
- package/dist/rules/request-headers.d.ts +61 -0
- package/dist/rules/request-headers.js +113 -0
- package/dist/rules/user-agent.d.ts +25 -0
- package/dist/rules/user-agent.js +212 -0
- package/dist/serialize.d.ts +12 -0
- package/dist/serialize.js +23 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @ressjs/platform
|
|
2
|
+
|
|
3
|
+
Responde una sola pregunta: **qué es el cliente que está del otro lado**.
|
|
4
|
+
|
|
5
|
+
La responden igual el router, el puente del WebView, el cliente y cualquier
|
|
6
|
+
consumidor externo. Por eso vive en su propio paquete y no depende de nada.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { detect } from '@ressjs/platform'
|
|
10
|
+
|
|
11
|
+
const platform = detect({ userAgent, headers })
|
|
12
|
+
// { webview: false, os: 'tizen', device: 'tv', ... }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## El modelo
|
|
16
|
+
|
|
17
|
+
Tres ejes ortogonales. Cualquier combinación es expresable.
|
|
18
|
+
|
|
19
|
+
| Eje | Valores | Qué describe |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `context` | `webview` | la página está incrustada dentro de una app; su ausencia es un navegador |
|
|
22
|
+
| `os` | `ios` `android` `tizen` `webos` `windows` `macos` `linux` | sistema operativo |
|
|
23
|
+
| `device` | `phone` `tablet` `tv` `desktop` | forma del aparato y modo de operación |
|
|
24
|
+
|
|
25
|
+
Los datos de la app que embebe la página —cuál es, en qué versión, sobre qué
|
|
26
|
+
versión del sistema— llegan en `nativeApp` y **nunca** eligen qué archivo servir.
|
|
27
|
+
|
|
28
|
+
## Cómo se decide
|
|
29
|
+
|
|
30
|
+
Seis capas, de lo que el cliente afirma a lo que se adivina. Cada una aporta
|
|
31
|
+
sólo los campos que puede determinar, y un campo ya resuelto no se sobrescribe.
|
|
32
|
+
Ninguna capa devuelve un resultado completo ni corta la evaluación: por eso
|
|
33
|
+
ninguna puede volver inalcanzable a la siguiente.
|
|
34
|
+
|
|
35
|
+
1. `override` — forzado para depurar, apagado salvo que se habilite
|
|
36
|
+
2. `explicit-headers` — el contrato v2
|
|
37
|
+
3. `legacy-headers` — el contrato v1 de los clientes ya publicados
|
|
38
|
+
4. `client-hints` — lo que el navegador manda por su cuenta
|
|
39
|
+
5. `user-agent` — la tabla de reglas
|
|
40
|
+
6. `default` — lo que quede sin resolver
|
|
41
|
+
|
|
42
|
+
Cada campo del resultado declara de qué capa salió y con cuánta confianza:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
platform.detection.fields.device
|
|
46
|
+
// { layer: 'user-agent', confidence: 'high', rule: 'ua:android-tv' }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Un `webview: false` que nadie afirmó se distingue así de uno detectado.
|
|
50
|
+
|
|
51
|
+
## Cabeceras
|
|
52
|
+
|
|
53
|
+
| Cabecera | Aporta |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `x-ressjs-webview` | `webview` |
|
|
56
|
+
| `x-ressjs-os` | `os` |
|
|
57
|
+
| `x-ressjs-device` | `device` |
|
|
58
|
+
| `x-ressjs-app-id` | `nativeApp.id`, e implica `webview: true` |
|
|
59
|
+
| `x-ressjs-app-version` | `nativeApp.version` |
|
|
60
|
+
| `x-ressjs-os-version` | `nativeApp.osVersion` |
|
|
61
|
+
| `x-ressjs-capabilities` | `capabilities`, separadas por comas |
|
|
62
|
+
|
|
63
|
+
Las del contrato anterior —`x-ressjs-platform`, `x-webview`, `x-rn-platform`,
|
|
64
|
+
`x-ressjs-version`— se siguen aceptando y se traducen.
|
|
65
|
+
|
|
66
|
+
## Agregar una plataforma
|
|
67
|
+
|
|
68
|
+
Es un dato, no un cambio de código:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const registry = createPlatformRegistry({
|
|
72
|
+
values: [{ token: 'kaios', axis: 'os' }],
|
|
73
|
+
detect: [
|
|
74
|
+
{
|
|
75
|
+
name: 'ua:kaios',
|
|
76
|
+
test: /KAIOS/i,
|
|
77
|
+
set: { os: 'kaios', device: 'phone' },
|
|
78
|
+
confidence: 'high',
|
|
79
|
+
priority: 150,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A partir de ahí `index.kaios.scss` es una variante válida.
|
|
86
|
+
|
|
87
|
+
## Desde el navegador
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { getPlatform } from '@ressjs/platform/client'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Lee lo que el servidor serializó en `window.__RESS_PLATFORM__`, de modo que
|
|
94
|
+
cliente y servidor coinciden exactamente. Sólo detecta por su cuenta si no hay
|
|
95
|
+
nada serializado.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* La plataforma, del lado del navegador.
|
|
3
|
+
*
|
|
4
|
+
* Lo normal es que el servidor ya la haya resuelto y serializado en el HTML: el
|
|
5
|
+
* cliente la lee de ahí en vez de volver a detectarla, y así el marcado que
|
|
6
|
+
* hidrata coincide exactamente con el que se envió. Detectar de nuevo abriría la
|
|
7
|
+
* puerta a que servidor y cliente discrepen, que es la clase de desajuste que
|
|
8
|
+
* rompe la hidratación de React.
|
|
9
|
+
*/
|
|
10
|
+
import type { PlatformInfo } from './model/types';
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
__RESS_PLATFORM__?: PlatformInfo;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Qué plataforma es esta.
|
|
18
|
+
*
|
|
19
|
+
* Sólo detecta por su cuenta si el servidor no dejó nada, que es lo que pasa en
|
|
20
|
+
* una página estática servida desde un CDN.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getPlatform(): PlatformInfo;
|
|
23
|
+
/**
|
|
24
|
+
* Detección desde el navegador, con las señales que sólo existen acá.
|
|
25
|
+
*
|
|
26
|
+
* `window.ReactNativeWebView` es una certeza y no una heurística: el puente sólo
|
|
27
|
+
* existe si el contenedor lo inyectó.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectClient(): PlatformInfo;
|
|
30
|
+
export { DEFAULT_PLATFORM } from './model/defaults';
|
|
31
|
+
export { hasCapability, isHandheld, isTV, isWebView } from './predicates';
|
|
32
|
+
export type { NativeAppInfo, PlatformDevice, PlatformInfo, PlatformOS } from './model/types';
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* La plataforma, del lado del navegador.
|
|
4
|
+
*
|
|
5
|
+
* Lo normal es que el servidor ya la haya resuelto y serializado en el HTML: el
|
|
6
|
+
* cliente la lee de ahí en vez de volver a detectarla, y así el marcado que
|
|
7
|
+
* hidrata coincide exactamente con el que se envió. Detectar de nuevo abriría la
|
|
8
|
+
* puerta a que servidor y cliente discrepen, que es la clase de desajuste que
|
|
9
|
+
* rompe la hidratación de React.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.isWebView = exports.isTV = exports.isHandheld = exports.hasCapability = exports.DEFAULT_PLATFORM = void 0;
|
|
13
|
+
exports.getPlatform = getPlatform;
|
|
14
|
+
exports.detectClient = detectClient;
|
|
15
|
+
const defaults_1 = require("./model/defaults");
|
|
16
|
+
const pipeline_1 = require("./detect/pipeline");
|
|
17
|
+
/**
|
|
18
|
+
* Qué plataforma es esta.
|
|
19
|
+
*
|
|
20
|
+
* Sólo detecta por su cuenta si el servidor no dejó nada, que es lo que pasa en
|
|
21
|
+
* una página estática servida desde un CDN.
|
|
22
|
+
*/
|
|
23
|
+
function getPlatform() {
|
|
24
|
+
if (typeof window === 'undefined')
|
|
25
|
+
return defaults_1.DEFAULT_PLATFORM;
|
|
26
|
+
return window.__RESS_PLATFORM__ ?? detectClient();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Detección desde el navegador, con las señales que sólo existen acá.
|
|
30
|
+
*
|
|
31
|
+
* `window.ReactNativeWebView` es una certeza y no una heurística: el puente sólo
|
|
32
|
+
* existe si el contenedor lo inyectó.
|
|
33
|
+
*/
|
|
34
|
+
function detectClient() {
|
|
35
|
+
if (typeof navigator === 'undefined')
|
|
36
|
+
return defaults_1.DEFAULT_PLATFORM;
|
|
37
|
+
const base = (0, pipeline_1.detect)({ userAgent: navigator.userAgent });
|
|
38
|
+
const inReactNativeWebView = typeof window !== 'undefined' && 'ReactNativeWebView' in window;
|
|
39
|
+
if (!inReactNativeWebView || base.webview)
|
|
40
|
+
return base;
|
|
41
|
+
return {
|
|
42
|
+
...base,
|
|
43
|
+
webview: true,
|
|
44
|
+
detection: {
|
|
45
|
+
...base.detection,
|
|
46
|
+
fields: {
|
|
47
|
+
...base.detection.fields,
|
|
48
|
+
context: { layer: 'client-signals', confidence: 'exact', rule: 'ReactNativeWebView' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
var defaults_2 = require("./model/defaults");
|
|
54
|
+
Object.defineProperty(exports, "DEFAULT_PLATFORM", { enumerable: true, get: function () { return defaults_2.DEFAULT_PLATFORM; } });
|
|
55
|
+
var predicates_1 = require("./predicates");
|
|
56
|
+
Object.defineProperty(exports, "hasCapability", { enumerable: true, get: function () { return predicates_1.hasCapability; } });
|
|
57
|
+
Object.defineProperty(exports, "isHandheld", { enumerable: true, get: function () { return predicates_1.isHandheld; } });
|
|
58
|
+
Object.defineProperty(exports, "isTV", { enumerable: true, get: function () { return predicates_1.isTV; } });
|
|
59
|
+
Object.defineProperty(exports, "isWebView", { enumerable: true, get: function () { return predicates_1.isWebView; } });
|
package/dist/compat.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PlatformInfo } from './model/types';
|
|
2
|
+
import type { Headers } from './rules/headers';
|
|
3
|
+
/**
|
|
4
|
+
* Firma del modelo v1, conservada para no obligar a tocar a quien ya la usa.
|
|
5
|
+
*
|
|
6
|
+
* Lo nuevo se escribe contra `detect()`, que recibe un objeto y admite el
|
|
7
|
+
* registro del proyecto.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectPlatform(userAgent: string, headers?: Headers): PlatformInfo;
|
package/dist/compat.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectPlatform = detectPlatform;
|
|
4
|
+
const pipeline_1 = require("./detect/pipeline");
|
|
5
|
+
/**
|
|
6
|
+
* Firma del modelo v1, conservada para no obligar a tocar a quien ya la usa.
|
|
7
|
+
*
|
|
8
|
+
* Lo nuevo se escribe contra `detect()`, que recibe un objeto y admite el
|
|
9
|
+
* registro del proyecto.
|
|
10
|
+
*/
|
|
11
|
+
function detectPlatform(userAgent, headers = {}) {
|
|
12
|
+
return (0, pipeline_1.detect)({ userAgent, headers });
|
|
13
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DetectionLayerId } from '../model/types';
|
|
2
|
+
import type { PlatformRegistry } from '../registry/types';
|
|
3
|
+
import type { Headers, LayerResult } from '../rules/headers';
|
|
4
|
+
/** Lo que ve una capa. */
|
|
5
|
+
export interface DetectionInput {
|
|
6
|
+
userAgent?: string;
|
|
7
|
+
headers?: Headers;
|
|
8
|
+
/** Permite forzar una plataforma para reproducir un caso. Apagado por defecto. */
|
|
9
|
+
allowOverride?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Si la tabla de User-Agent participa de la decisión.
|
|
12
|
+
*
|
|
13
|
+
* Se apaga cuando la respuesta no va a declarar `Vary: User-Agent`. Resolver
|
|
14
|
+
* por algo que no se declara es servir la variante equivocada apenas haya una
|
|
15
|
+
* caché intermedia: mejor no usarlo que usarlo a escondidas.
|
|
16
|
+
*/
|
|
17
|
+
useUserAgent?: boolean;
|
|
18
|
+
registry?: PlatformRegistry;
|
|
19
|
+
}
|
|
20
|
+
export interface DetectionLayer {
|
|
21
|
+
id: DetectionLayerId;
|
|
22
|
+
read(input: DetectionInput, registry: PlatformRegistry): LayerResult;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Las capas, en orden de precedencia. **Este es el único lugar donde ese orden
|
|
26
|
+
* existe**: no hay ninguna otra parte del sistema con un criterio propio.
|
|
27
|
+
*
|
|
28
|
+
* Van de lo que el cliente afirma a lo que se adivina:
|
|
29
|
+
*
|
|
30
|
+
* 1. `override` — forzado para depurar, sólo si está habilitado
|
|
31
|
+
* 2. `explicit-headers` — el contrato v2, el cliente diciendo lo que es
|
|
32
|
+
* 3. `legacy-headers` — el contrato v1 de los clientes ya publicados
|
|
33
|
+
* 4. `edge` — lo que la red de distribución ya resolvió, si el proyecto la declaró
|
|
34
|
+
* 5. `client-hints` — lo que el navegador manda por su cuenta
|
|
35
|
+
* 6. `user-agent` — la tabla de reglas
|
|
36
|
+
*
|
|
37
|
+
* La capa `default` no está en la lista porque no lee nada: es lo que queda
|
|
38
|
+
* cuando ninguna de las anteriores resolvió un campo.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DETECTION_LAYERS: readonly DetectionLayer[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DETECTION_LAYERS = void 0;
|
|
4
|
+
const headers_1 = require("../rules/headers");
|
|
5
|
+
const edge_1 = require("../rules/edge");
|
|
6
|
+
const apply_1 = require("../rules/apply");
|
|
7
|
+
const override_1 = require("./override");
|
|
8
|
+
/**
|
|
9
|
+
* Las capas, en orden de precedencia. **Este es el único lugar donde ese orden
|
|
10
|
+
* existe**: no hay ninguna otra parte del sistema con un criterio propio.
|
|
11
|
+
*
|
|
12
|
+
* Van de lo que el cliente afirma a lo que se adivina:
|
|
13
|
+
*
|
|
14
|
+
* 1. `override` — forzado para depurar, sólo si está habilitado
|
|
15
|
+
* 2. `explicit-headers` — el contrato v2, el cliente diciendo lo que es
|
|
16
|
+
* 3. `legacy-headers` — el contrato v1 de los clientes ya publicados
|
|
17
|
+
* 4. `edge` — lo que la red de distribución ya resolvió, si el proyecto la declaró
|
|
18
|
+
* 5. `client-hints` — lo que el navegador manda por su cuenta
|
|
19
|
+
* 6. `user-agent` — la tabla de reglas
|
|
20
|
+
*
|
|
21
|
+
* La capa `default` no está en la lista porque no lee nada: es lo que queda
|
|
22
|
+
* cuando ninguna de las anteriores resolvió un campo.
|
|
23
|
+
*/
|
|
24
|
+
exports.DETECTION_LAYERS = [
|
|
25
|
+
{ id: 'override', read: (input) => (input.allowOverride ? (0, override_1.readOverride)(input.headers ?? {}) : {}) },
|
|
26
|
+
{ id: 'explicit-headers', read: (input) => (0, headers_1.readExplicitHeaders)(input.headers ?? {}) },
|
|
27
|
+
{ id: 'legacy-headers', read: (input) => (0, headers_1.readLegacyHeaders)(input.headers ?? {}) },
|
|
28
|
+
{ id: 'edge', read: (input, registry) => (0, edge_1.readEdgeHeaders)(input.headers ?? {}, registry.edge) },
|
|
29
|
+
{ id: 'client-hints', read: (input) => (0, headers_1.readClientHints)(input.headers ?? {}) },
|
|
30
|
+
{
|
|
31
|
+
id: 'user-agent',
|
|
32
|
+
read: (input, registry) => (input.useUserAgent ?? true) ? (0, apply_1.readUserAgent)(input.userAgent ?? '', registry) : {},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Axis } from '../model/known';
|
|
2
|
+
import type { DetectionLayerId, FieldOrigin, NativeAppInfo } from '../model/types';
|
|
3
|
+
import type { LayerResult } from '../rules/headers';
|
|
4
|
+
/**
|
|
5
|
+
* Lo que el pipeline va llenando: cada eje se fija una sola vez.
|
|
6
|
+
*
|
|
7
|
+
* Es un acumulador y no un `PlatformInfo` a medio construir a propósito: los
|
|
8
|
+
* campos que todavía nadie resolvió están ausentes, no puestos en un valor por
|
|
9
|
+
* defecto que después habría que distinguir del valor real.
|
|
10
|
+
*/
|
|
11
|
+
export interface Accumulator {
|
|
12
|
+
webview?: boolean;
|
|
13
|
+
os?: string;
|
|
14
|
+
device?: string;
|
|
15
|
+
nativeApp?: Partial<NativeAppInfo>;
|
|
16
|
+
capabilities: string[];
|
|
17
|
+
fields: Partial<Record<Axis, FieldOrigin>>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createAccumulator(): Accumulator;
|
|
20
|
+
/**
|
|
21
|
+
* Incorpora lo que aportó una capa. **El primero que fija un campo gana.**
|
|
22
|
+
*
|
|
23
|
+
* Esa regla es lo que vuelve imposible el defecto que motivaba esta spec: no hay
|
|
24
|
+
* ninguna capa que devuelva un resultado completo ni que corte la evaluación, así
|
|
25
|
+
* que ninguna puede volver inalcanzable a la siguiente. Cada una aporta lo que
|
|
26
|
+
* sabe y lo que no sabe lo resuelven las de abajo.
|
|
27
|
+
*/
|
|
28
|
+
export declare function absorb(acc: Accumulator, layer: DetectionLayerId, result: LayerResult): void;
|
|
29
|
+
/** Los tres ejes ya resueltos, o `false` si alguno falta. */
|
|
30
|
+
export declare function isComplete(acc: Accumulator): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAccumulator = createAccumulator;
|
|
4
|
+
exports.absorb = absorb;
|
|
5
|
+
exports.isComplete = isComplete;
|
|
6
|
+
function createAccumulator() {
|
|
7
|
+
return { capabilities: [], fields: {} };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Incorpora lo que aportó una capa. **El primero que fija un campo gana.**
|
|
11
|
+
*
|
|
12
|
+
* Esa regla es lo que vuelve imposible el defecto que motivaba esta spec: no hay
|
|
13
|
+
* ninguna capa que devuelva un resultado completo ni que corte la evaluación, así
|
|
14
|
+
* que ninguna puede volver inalcanzable a la siguiente. Cada una aporta lo que
|
|
15
|
+
* sabe y lo que no sabe lo resuelven las de abajo.
|
|
16
|
+
*/
|
|
17
|
+
function absorb(acc, layer, result) {
|
|
18
|
+
if (result.webview !== undefined && acc.webview === undefined) {
|
|
19
|
+
acc.webview = result.webview.value;
|
|
20
|
+
acc.fields.context = origin(layer, result.webview.confidence, result.webview.rule);
|
|
21
|
+
}
|
|
22
|
+
if (result.os !== undefined && acc.os === undefined) {
|
|
23
|
+
acc.os = result.os.value;
|
|
24
|
+
acc.fields.os = origin(layer, result.os.confidence, result.os.rule);
|
|
25
|
+
}
|
|
26
|
+
if (result.device !== undefined && acc.device === undefined) {
|
|
27
|
+
acc.device = result.device.value;
|
|
28
|
+
acc.fields.device = origin(layer, result.device.confidence, result.device.rule);
|
|
29
|
+
}
|
|
30
|
+
// Los metadatos se acumulan campo a campo, con la misma regla: el primero que
|
|
31
|
+
// lo dijo gana. Un cliente puede declarar su versión por el contrato nuevo y
|
|
32
|
+
// su identificador por el viejo.
|
|
33
|
+
if (result.nativeApp) {
|
|
34
|
+
const merged = { ...result.nativeApp, ...acc.nativeApp };
|
|
35
|
+
acc.nativeApp = Object.fromEntries(Object.entries(merged).filter(([, v]) => v !== undefined));
|
|
36
|
+
}
|
|
37
|
+
if (result.capabilities?.length) {
|
|
38
|
+
for (const capability of result.capabilities) {
|
|
39
|
+
if (!acc.capabilities.includes(capability))
|
|
40
|
+
acc.capabilities.push(capability);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function origin(layer, confidence, rule) {
|
|
45
|
+
return rule ? { layer, confidence, rule } : { layer, confidence };
|
|
46
|
+
}
|
|
47
|
+
/** Los tres ejes ya resueltos, o `false` si alguno falta. */
|
|
48
|
+
function isComplete(acc) {
|
|
49
|
+
return acc.webview !== undefined && acc.os !== undefined && acc.device !== undefined;
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Headers, type LayerResult } from '../rules/headers';
|
|
2
|
+
/**
|
|
3
|
+
* Forzado de plataforma para reproducir un caso sin tener el aparato a mano.
|
|
4
|
+
*
|
|
5
|
+
* Está apagado salvo que quien monta el servidor lo habilite explícitamente: es
|
|
6
|
+
* una cabecera que cualquiera puede mandar, y con ella se elige qué variante
|
|
7
|
+
* recibe. En producción eso es una superficie de ataque, no una comodidad.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readOverride(headers: Headers): LayerResult;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readOverride = readOverride;
|
|
4
|
+
const headers_1 = require("../rules/headers");
|
|
5
|
+
const request_headers_1 = require("../rules/request-headers");
|
|
6
|
+
/**
|
|
7
|
+
* Forzado de plataforma para reproducir un caso sin tener el aparato a mano.
|
|
8
|
+
*
|
|
9
|
+
* Está apagado salvo que quien monta el servidor lo habilite explícitamente: es
|
|
10
|
+
* una cabecera que cualquiera puede mandar, y con ella se elige qué variante
|
|
11
|
+
* recibe. En producción eso es una superficie de ataque, no una comodidad.
|
|
12
|
+
*/
|
|
13
|
+
function readOverride(headers) {
|
|
14
|
+
const raw = (0, headers_1.header)(headers, request_headers_1.HEADER_OVERRIDE);
|
|
15
|
+
if (!raw)
|
|
16
|
+
return {};
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const part of raw.split(/[,;]/)) {
|
|
19
|
+
const [key, value] = part.split('=').map((s) => s.trim().toLowerCase());
|
|
20
|
+
if (!key || !value)
|
|
21
|
+
continue;
|
|
22
|
+
if (key === 'webview') {
|
|
23
|
+
result.webview = { value: value !== 'false' && value !== '0', confidence: 'exact', rule: 'override' };
|
|
24
|
+
}
|
|
25
|
+
else if (key === 'os') {
|
|
26
|
+
result.os = { value, confidence: 'exact', rule: 'override' };
|
|
27
|
+
}
|
|
28
|
+
else if (key === 'device') {
|
|
29
|
+
result.device = { value, confidence: 'exact', rule: 'override' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PlatformInfo } from '../model/types';
|
|
2
|
+
import { type DetectionInput } from './layers';
|
|
3
|
+
/**
|
|
4
|
+
* Qué es el cliente que está del otro lado.
|
|
5
|
+
*
|
|
6
|
+
* Es una función pura: las mismas entradas producen siempre la misma salida, sin
|
|
7
|
+
* estado, sin reloj, sin red ni disco. Es lo que permite correrla igual en el
|
|
8
|
+
* servidor y en el navegador, y probarla sin montar nada.
|
|
9
|
+
*/
|
|
10
|
+
export declare function detect(input?: DetectionInput): PlatformInfo;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detect = detect;
|
|
4
|
+
const defaults_1 = require("../model/defaults");
|
|
5
|
+
const create_1 = require("../registry/create");
|
|
6
|
+
const merge_1 = require("./merge");
|
|
7
|
+
const layers_1 = require("./layers");
|
|
8
|
+
/**
|
|
9
|
+
* Qué es el cliente que está del otro lado.
|
|
10
|
+
*
|
|
11
|
+
* Es una función pura: las mismas entradas producen siempre la misma salida, sin
|
|
12
|
+
* estado, sin reloj, sin red ni disco. Es lo que permite correrla igual en el
|
|
13
|
+
* servidor y en el navegador, y probarla sin montar nada.
|
|
14
|
+
*/
|
|
15
|
+
function detect(input = {}) {
|
|
16
|
+
const registry = input.registry ?? create_1.defaultPlatformRegistry;
|
|
17
|
+
const acc = (0, merge_1.createAccumulator)();
|
|
18
|
+
for (const layer of layers_1.DETECTION_LAYERS) {
|
|
19
|
+
if ((0, merge_1.isComplete)(acc) && acc.capabilities.length > 0)
|
|
20
|
+
break;
|
|
21
|
+
(0, merge_1.absorb)(acc, layer.id, layer.read(input, registry));
|
|
22
|
+
}
|
|
23
|
+
const fields = {
|
|
24
|
+
context: acc.fields.context ?? defaults_1.DEFAULT_PLATFORM.detection.fields.context,
|
|
25
|
+
os: acc.fields.os ?? defaults_1.DEFAULT_PLATFORM.detection.fields.os,
|
|
26
|
+
device: acc.fields.device ?? defaults_1.DEFAULT_PLATFORM.detection.fields.device,
|
|
27
|
+
};
|
|
28
|
+
const info = {
|
|
29
|
+
webview: acc.webview ?? defaults_1.DEFAULT_PLATFORM.webview,
|
|
30
|
+
os: acc.os ?? defaults_1.DEFAULT_PLATFORM.os,
|
|
31
|
+
device: acc.device ?? defaults_1.DEFAULT_PLATFORM.device,
|
|
32
|
+
capabilities: acc.capabilities,
|
|
33
|
+
detection: {
|
|
34
|
+
source: weakestLayer(fields),
|
|
35
|
+
confidence: weakestConfidence(fields),
|
|
36
|
+
fields,
|
|
37
|
+
},
|
|
38
|
+
source: cameFromHeaders(fields) ? 'headers' : 'user-agent',
|
|
39
|
+
};
|
|
40
|
+
// Basta con que el cliente haya declarado algo: un contrato viejo puede
|
|
41
|
+
// aportar la versión sin poder aportar el identificador.
|
|
42
|
+
if (acc.nativeApp && Object.keys(acc.nativeApp).length > 0) {
|
|
43
|
+
info.nativeApp = { ...acc.nativeApp };
|
|
44
|
+
}
|
|
45
|
+
return info;
|
|
46
|
+
}
|
|
47
|
+
/** Orden de las capas, de la que más afirma a la que menos. */
|
|
48
|
+
const LAYER_ORDER = [
|
|
49
|
+
'override',
|
|
50
|
+
'explicit-headers',
|
|
51
|
+
'legacy-headers',
|
|
52
|
+
'edge',
|
|
53
|
+
'client-hints',
|
|
54
|
+
'user-agent',
|
|
55
|
+
'client-signals',
|
|
56
|
+
'default',
|
|
57
|
+
];
|
|
58
|
+
const CONFIDENCE_ORDER = ['exact', 'high', 'medium', 'low', 'none'];
|
|
59
|
+
/**
|
|
60
|
+
* La confianza del resultado es la del eje peor resuelto.
|
|
61
|
+
*
|
|
62
|
+
* Reportar el promedio, o la del mejor, escondería justamente el campo del que
|
|
63
|
+
* hay que desconfiar.
|
|
64
|
+
*/
|
|
65
|
+
function weakestConfidence(fields) {
|
|
66
|
+
return Object.values(fields).reduce((worst, f) => CONFIDENCE_ORDER.indexOf(f.confidence) > CONFIDENCE_ORDER.indexOf(worst)
|
|
67
|
+
? f.confidence
|
|
68
|
+
: worst, 'exact');
|
|
69
|
+
}
|
|
70
|
+
function weakestLayer(fields) {
|
|
71
|
+
return Object.values(fields).reduce((worst, f) => LAYER_ORDER.indexOf(f.layer) > LAYER_ORDER.indexOf(worst) ? f.layer : worst, 'override');
|
|
72
|
+
}
|
|
73
|
+
function cameFromHeaders(fields) {
|
|
74
|
+
return Object.values(fields).some((f) => f.layer === 'explicit-headers' || f.layer === 'legacy-headers');
|
|
75
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelo de plataforma de ress.js.
|
|
3
|
+
*
|
|
4
|
+
* Responde una sola pregunta —qué es el cliente que está del otro lado— y la
|
|
5
|
+
* responde igual para todos: el router, el puente del WebView, el cliente y
|
|
6
|
+
* cualquier consumidor externo. Por eso vive en su propio paquete y no depende
|
|
7
|
+
* de nada.
|
|
8
|
+
*/
|
|
9
|
+
export { detect } from './detect/pipeline';
|
|
10
|
+
export { detectPlatform } from './compat';
|
|
11
|
+
export { DETECTION_LAYERS } from './detect/layers';
|
|
12
|
+
export type { DetectionInput, DetectionLayer } from './detect/layers';
|
|
13
|
+
export { createPlatformRegistry, defaultPlatformRegistry } from './registry/create';
|
|
14
|
+
export { parseVariantSuffix } from './registry/parse';
|
|
15
|
+
export { ALL_AXES, DEFAULT_AXIS_PRECEDENCE, DEFAULT_VALUES, RETIRED_TOKENS, } from './registry/defaults';
|
|
16
|
+
export { DEFAULT_RULES } from './rules/user-agent';
|
|
17
|
+
export { EDGE_PRESETS, readEdgeHeaders } from './rules/edge';
|
|
18
|
+
export type { EdgeHintRule } from './rules/edge';
|
|
19
|
+
export { HEADER_OVERRIDE, HEADERS_CLIENT_HINTS, HEADERS_LEGACY, HEADERS_V2, PLATFORM_REQUEST_HEADERS, PLATFORM_VARY_HEADERS, HEADERS_BY_AXIS, varyHeadersForAxes, } from './rules/request-headers';
|
|
20
|
+
export { DEFAULT_PLATFORM } from './model/defaults';
|
|
21
|
+
export { hasCapability, isHandheld, isTV, isWebView } from './predicates';
|
|
22
|
+
export { platformScriptTag, serializePlatform } from './serialize';
|
|
23
|
+
export type { Axis, KnownContext, KnownDevice, KnownOS } from './model/known';
|
|
24
|
+
export type { Confidence, DetectionLayerId, FieldOrigin, NativeAppInfo, PlatformDevice, PlatformInfo, PlatformOS, PlatformValueMap, } from './model/types';
|
|
25
|
+
export type { AxisValueDef, PlatformRegistry, RegistryOptions, RetiredTokenDef, RuleEffect, UserAgentRule, VariantTag, } from './registry/types';
|
|
26
|
+
export type { Headers } from './rules/headers';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Modelo de plataforma de ress.js.
|
|
4
|
+
*
|
|
5
|
+
* Responde una sola pregunta —qué es el cliente que está del otro lado— y la
|
|
6
|
+
* responde igual para todos: el router, el puente del WebView, el cliente y
|
|
7
|
+
* cualquier consumidor externo. Por eso vive en su propio paquete y no depende
|
|
8
|
+
* de nada.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.serializePlatform = exports.platformScriptTag = exports.isWebView = exports.isTV = exports.isHandheld = exports.hasCapability = exports.DEFAULT_PLATFORM = exports.varyHeadersForAxes = exports.HEADERS_BY_AXIS = exports.PLATFORM_VARY_HEADERS = exports.PLATFORM_REQUEST_HEADERS = exports.HEADERS_V2 = exports.HEADERS_LEGACY = exports.HEADERS_CLIENT_HINTS = exports.HEADER_OVERRIDE = exports.readEdgeHeaders = exports.EDGE_PRESETS = exports.DEFAULT_RULES = exports.RETIRED_TOKENS = exports.DEFAULT_VALUES = exports.DEFAULT_AXIS_PRECEDENCE = exports.ALL_AXES = exports.parseVariantSuffix = exports.defaultPlatformRegistry = exports.createPlatformRegistry = exports.DETECTION_LAYERS = exports.detectPlatform = exports.detect = void 0;
|
|
12
|
+
var pipeline_1 = require("./detect/pipeline");
|
|
13
|
+
Object.defineProperty(exports, "detect", { enumerable: true, get: function () { return pipeline_1.detect; } });
|
|
14
|
+
var compat_1 = require("./compat");
|
|
15
|
+
Object.defineProperty(exports, "detectPlatform", { enumerable: true, get: function () { return compat_1.detectPlatform; } });
|
|
16
|
+
var layers_1 = require("./detect/layers");
|
|
17
|
+
Object.defineProperty(exports, "DETECTION_LAYERS", { enumerable: true, get: function () { return layers_1.DETECTION_LAYERS; } });
|
|
18
|
+
var create_1 = require("./registry/create");
|
|
19
|
+
Object.defineProperty(exports, "createPlatformRegistry", { enumerable: true, get: function () { return create_1.createPlatformRegistry; } });
|
|
20
|
+
Object.defineProperty(exports, "defaultPlatformRegistry", { enumerable: true, get: function () { return create_1.defaultPlatformRegistry; } });
|
|
21
|
+
var parse_1 = require("./registry/parse");
|
|
22
|
+
Object.defineProperty(exports, "parseVariantSuffix", { enumerable: true, get: function () { return parse_1.parseVariantSuffix; } });
|
|
23
|
+
var defaults_1 = require("./registry/defaults");
|
|
24
|
+
Object.defineProperty(exports, "ALL_AXES", { enumerable: true, get: function () { return defaults_1.ALL_AXES; } });
|
|
25
|
+
Object.defineProperty(exports, "DEFAULT_AXIS_PRECEDENCE", { enumerable: true, get: function () { return defaults_1.DEFAULT_AXIS_PRECEDENCE; } });
|
|
26
|
+
Object.defineProperty(exports, "DEFAULT_VALUES", { enumerable: true, get: function () { return defaults_1.DEFAULT_VALUES; } });
|
|
27
|
+
Object.defineProperty(exports, "RETIRED_TOKENS", { enumerable: true, get: function () { return defaults_1.RETIRED_TOKENS; } });
|
|
28
|
+
var user_agent_1 = require("./rules/user-agent");
|
|
29
|
+
Object.defineProperty(exports, "DEFAULT_RULES", { enumerable: true, get: function () { return user_agent_1.DEFAULT_RULES; } });
|
|
30
|
+
var edge_1 = require("./rules/edge");
|
|
31
|
+
Object.defineProperty(exports, "EDGE_PRESETS", { enumerable: true, get: function () { return edge_1.EDGE_PRESETS; } });
|
|
32
|
+
Object.defineProperty(exports, "readEdgeHeaders", { enumerable: true, get: function () { return edge_1.readEdgeHeaders; } });
|
|
33
|
+
var request_headers_1 = require("./rules/request-headers");
|
|
34
|
+
Object.defineProperty(exports, "HEADER_OVERRIDE", { enumerable: true, get: function () { return request_headers_1.HEADER_OVERRIDE; } });
|
|
35
|
+
Object.defineProperty(exports, "HEADERS_CLIENT_HINTS", { enumerable: true, get: function () { return request_headers_1.HEADERS_CLIENT_HINTS; } });
|
|
36
|
+
Object.defineProperty(exports, "HEADERS_LEGACY", { enumerable: true, get: function () { return request_headers_1.HEADERS_LEGACY; } });
|
|
37
|
+
Object.defineProperty(exports, "HEADERS_V2", { enumerable: true, get: function () { return request_headers_1.HEADERS_V2; } });
|
|
38
|
+
Object.defineProperty(exports, "PLATFORM_REQUEST_HEADERS", { enumerable: true, get: function () { return request_headers_1.PLATFORM_REQUEST_HEADERS; } });
|
|
39
|
+
Object.defineProperty(exports, "PLATFORM_VARY_HEADERS", { enumerable: true, get: function () { return request_headers_1.PLATFORM_VARY_HEADERS; } });
|
|
40
|
+
Object.defineProperty(exports, "HEADERS_BY_AXIS", { enumerable: true, get: function () { return request_headers_1.HEADERS_BY_AXIS; } });
|
|
41
|
+
Object.defineProperty(exports, "varyHeadersForAxes", { enumerable: true, get: function () { return request_headers_1.varyHeadersForAxes; } });
|
|
42
|
+
var defaults_2 = require("./model/defaults");
|
|
43
|
+
Object.defineProperty(exports, "DEFAULT_PLATFORM", { enumerable: true, get: function () { return defaults_2.DEFAULT_PLATFORM; } });
|
|
44
|
+
var predicates_1 = require("./predicates");
|
|
45
|
+
Object.defineProperty(exports, "hasCapability", { enumerable: true, get: function () { return predicates_1.hasCapability; } });
|
|
46
|
+
Object.defineProperty(exports, "isHandheld", { enumerable: true, get: function () { return predicates_1.isHandheld; } });
|
|
47
|
+
Object.defineProperty(exports, "isTV", { enumerable: true, get: function () { return predicates_1.isTV; } });
|
|
48
|
+
Object.defineProperty(exports, "isWebView", { enumerable: true, get: function () { return predicates_1.isWebView; } });
|
|
49
|
+
var serialize_1 = require("./serialize");
|
|
50
|
+
Object.defineProperty(exports, "platformScriptTag", { enumerable: true, get: function () { return serialize_1.platformScriptTag; } });
|
|
51
|
+
Object.defineProperty(exports, "serializePlatform", { enumerable: true, get: function () { return serialize_1.serializePlatform; } });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PlatformInfo } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Lo que se responde cuando nadie dijo nada.
|
|
4
|
+
*
|
|
5
|
+
* Cada eje queda marcado con confianza `none`, que es lo que distingue este
|
|
6
|
+
* `webview: false` de uno que una capa afirmó.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_PLATFORM: PlatformInfo;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PLATFORM = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Lo que se responde cuando nadie dijo nada.
|
|
6
|
+
*
|
|
7
|
+
* Cada eje queda marcado con confianza `none`, que es lo que distingue este
|
|
8
|
+
* `webview: false` de uno que una capa afirmó.
|
|
9
|
+
*/
|
|
10
|
+
exports.DEFAULT_PLATFORM = Object.freeze({
|
|
11
|
+
webview: false,
|
|
12
|
+
os: 'unknown',
|
|
13
|
+
device: 'desktop',
|
|
14
|
+
capabilities: Object.freeze([]),
|
|
15
|
+
detection: Object.freeze({
|
|
16
|
+
source: 'default',
|
|
17
|
+
confidence: 'none',
|
|
18
|
+
fields: Object.freeze({
|
|
19
|
+
context: Object.freeze({ layer: 'default', confidence: 'none' }),
|
|
20
|
+
os: Object.freeze({ layer: 'default', confidence: 'none' }),
|
|
21
|
+
device: Object.freeze({ layer: 'default', confidence: 'none' }),
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
source: 'user-agent',
|
|
25
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulario que el framework conoce de fábrica.
|
|
3
|
+
*
|
|
4
|
+
* Un proyecto puede agregar valores a los ejes `os` y `device` desde su propia
|
|
5
|
+
* configuración; lo que no puede es agregar un eje. Agregar un valor es un dato,
|
|
6
|
+
* agregar un eje es un cambio de arquitectura.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Las tres preguntas con las que se describe un cliente.
|
|
10
|
+
*
|
|
11
|
+
* - `context`: ¿la página está incrustada dentro de una app?
|
|
12
|
+
* - `os`: ¿qué sistema operativo corre debajo?
|
|
13
|
+
* - `device`: ¿qué forma tiene el aparato y cómo se lo opera?
|
|
14
|
+
*
|
|
15
|
+
* Son ortogonales: cualquier combinación es expresable. Una WebView dentro de
|
|
16
|
+
* una app de Android TV es `webview` + `android` + `tv`.
|
|
17
|
+
*/
|
|
18
|
+
export type Axis = 'context' | 'os' | 'device';
|
|
19
|
+
/**
|
|
20
|
+
* Único token del eje `context`.
|
|
21
|
+
*
|
|
22
|
+
* No hay un token para "navegador" porque su ausencia ya lo dice, y porque un
|
|
23
|
+
* token que nombra la situación normal obliga a escribirlo en todas partes.
|
|
24
|
+
*/
|
|
25
|
+
export type KnownContext = 'webview';
|
|
26
|
+
/**
|
|
27
|
+
* `unknown` no es un token del registro: nadie puede escribir `index.unknown.scss`.
|
|
28
|
+
* Es el valor que toma `os` cuando ninguna capa de detección lo resolvió.
|
|
29
|
+
*/
|
|
30
|
+
export type KnownOS = 'ios' | 'android' | 'tizen' | 'webos' | 'windows' | 'macos' | 'linux' | 'unknown';
|
|
31
|
+
export type KnownDevice = 'phone' | 'tablet' | 'tv' | 'desktop';
|