@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.
Files changed (46) hide show
  1. package/README.md +95 -0
  2. package/dist/client.d.ts +32 -0
  3. package/dist/client.js +59 -0
  4. package/dist/compat.d.ts +9 -0
  5. package/dist/compat.js +13 -0
  6. package/dist/detect/layers.d.ts +40 -0
  7. package/dist/detect/layers.js +34 -0
  8. package/dist/detect/merge.d.ts +30 -0
  9. package/dist/detect/merge.js +50 -0
  10. package/dist/detect/override.d.ts +9 -0
  11. package/dist/detect/override.js +33 -0
  12. package/dist/detect/pipeline.d.ts +10 -0
  13. package/dist/detect/pipeline.js +75 -0
  14. package/dist/index.d.ts +26 -0
  15. package/dist/index.js +51 -0
  16. package/dist/model/defaults.d.ts +8 -0
  17. package/dist/model/defaults.js +25 -0
  18. package/dist/model/known.d.ts +31 -0
  19. package/dist/model/known.js +9 -0
  20. package/dist/model/types.d.ts +95 -0
  21. package/dist/model/types.js +2 -0
  22. package/dist/predicates.d.ts +9 -0
  23. package/dist/predicates.js +15 -0
  24. package/dist/registry/create.d.ts +17 -0
  25. package/dist/registry/create.js +87 -0
  26. package/dist/registry/defaults.d.ts +23 -0
  27. package/dist/registry/defaults.js +63 -0
  28. package/dist/registry/index.d.ts +11 -0
  29. package/dist/registry/index.js +20 -0
  30. package/dist/registry/parse.d.ts +14 -0
  31. package/dist/registry/parse.js +89 -0
  32. package/dist/registry/types.d.ts +91 -0
  33. package/dist/registry/types.js +2 -0
  34. package/dist/rules/apply.d.ts +13 -0
  35. package/dist/rules/apply.js +40 -0
  36. package/dist/rules/edge.d.ts +44 -0
  37. package/dist/rules/edge.js +66 -0
  38. package/dist/rules/headers.d.ts +48 -0
  39. package/dist/rules/headers.js +157 -0
  40. package/dist/rules/request-headers.d.ts +61 -0
  41. package/dist/rules/request-headers.js +113 -0
  42. package/dist/rules/user-agent.d.ts +25 -0
  43. package/dist/rules/user-agent.js +212 -0
  44. package/dist/serialize.d.ts +12 -0
  45. package/dist/serialize.js +23 -0
  46. package/package.json +48 -0
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * Vocabulario que el framework conoce de fábrica.
4
+ *
5
+ * Un proyecto puede agregar valores a los ejes `os` y `device` desde su propia
6
+ * configuración; lo que no puede es agregar un eje. Agregar un valor es un dato,
7
+ * agregar un eje es un cambio de arquitectura.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,95 @@
1
+ import type { Axis, KnownDevice, KnownOS } from './known';
2
+ /**
3
+ * Valores que un proyecto agrega a un eje.
4
+ *
5
+ * Se amplía por `declare module` desde el proyecto que consume el framework, de
6
+ * modo que sus tokens propios aparezcan en el autocompletado de su editor en vez
7
+ * de ser texto libre:
8
+ *
9
+ * ```ts
10
+ * declare module '@ressjs/platform' {
11
+ * interface PlatformValueMap {
12
+ * os: 'kaios'
13
+ * device: 'watch'
14
+ * }
15
+ * }
16
+ * ```
17
+ */
18
+ export interface PlatformValueMap {
19
+ }
20
+ /** Lo que el proyecto agregó a un eje, o nada si no agregó. */
21
+ type Extra<K extends string> = K extends keyof PlatformValueMap ? PlatformValueMap[K] : never;
22
+ /**
23
+ * Unión abierta que conserva las sugerencias.
24
+ *
25
+ * `KnownOS | string` colapsa a `string` y el editor deja de sugerir nada.
26
+ * `KnownOS | (string & {})` no colapsa: acepta cualquier cadena y sigue
27
+ * ofreciendo los valores conocidos.
28
+ */
29
+ type Open<Known extends string, Extra> = Known | (Extra extends string ? Extra : never) | (string & {});
30
+ export type PlatformOS = Open<KnownOS, Extra<'os'>>;
31
+ export type PlatformDevice = Open<KnownDevice, Extra<'device'>>;
32
+ /** De dónde salió un campo. El orden es el de precedencia. */
33
+ export type DetectionLayerId = 'override' | 'explicit-headers' | 'legacy-headers' | 'edge' | 'client-hints' | 'user-agent' | 'client-signals' | 'default';
34
+ /**
35
+ * Cuánto se puede confiar en un campo.
36
+ *
37
+ * `exact` es el cliente diciendo lo que es; `none` es nadie habiéndolo dicho.
38
+ * La distinción importa: un `webview: false` por defecto no significa lo mismo
39
+ * que uno afirmado.
40
+ */
41
+ export type Confidence = 'exact' | 'high' | 'medium' | 'low' | 'none';
42
+ /** Procedencia de un campo individual. */
43
+ export interface FieldOrigin {
44
+ layer: DetectionLayerId;
45
+ confidence: Confidence;
46
+ /** Nombre de la regla concreta que lo resolvió. Ej: `ua:tizen-smarttv`. */
47
+ rule?: string;
48
+ }
49
+ /**
50
+ * Datos de la app que embebe la página.
51
+ *
52
+ * Nunca participa de la resolución de variantes: qué app entró no cambia qué
53
+ * archivo servir. Sirve para decidir si una capacidad del puente existe en esa
54
+ * versión del cliente, para segmentar y para diagnosticar.
55
+ */
56
+ export interface NativeAppInfo {
57
+ /**
58
+ * Identificador de la app. Ej: `com.acme.player`, `react-native`.
59
+ *
60
+ * Opcional porque el contrato v1 no tenía forma de declararlo: un cliente ya
61
+ * publicado manda su versión sin poder decir quién es, y descartar ese dato
62
+ * por un campo que su contrato no ofrecía sería perder información que el
63
+ * cliente sí declaró.
64
+ */
65
+ id?: string;
66
+ /** Versión de la app. */
67
+ version?: string;
68
+ /** Versión del sistema operativo, tal como la reporta el cliente. */
69
+ osVersion?: string;
70
+ }
71
+ /** Qué es el cliente que está del otro lado de una petición. */
72
+ export interface PlatformInfo {
73
+ /** La página está incrustada dentro de una app. Su ausencia es un navegador. */
74
+ webview: boolean;
75
+ os: PlatformOS;
76
+ device: PlatformDevice;
77
+ /** Presente sólo cuando un cliente se declara. */
78
+ nativeApp?: NativeAppInfo;
79
+ /** Capacidades que el cliente declara. Ej: `['bridge','camera','dpad']`. */
80
+ capabilities: readonly string[];
81
+ detection: {
82
+ /** Capa de menor precedencia que aportó algún campo. */
83
+ source: DetectionLayerId;
84
+ /** La menor de las confianzas de los tres ejes. */
85
+ confidence: Confidence;
86
+ /** Qué resolvió cada eje. Esto es lo que se mira al depurar. */
87
+ fields: Record<Axis, FieldOrigin>;
88
+ };
89
+ /**
90
+ * @deprecated Modelo v1. Derivado: `headers` si algún eje vino de cabeceras,
91
+ * `user-agent` en cualquier otro caso.
92
+ */
93
+ source: 'headers' | 'user-agent';
94
+ }
95
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ import type { PlatformInfo } from './model/types';
2
+ /** La página corre dentro de una app y no en un navegador. */
3
+ export declare const isWebView: (p: PlatformInfo) => boolean;
4
+ /** Pantalla grande operada con control remoto. */
5
+ export declare const isTV: (p: PlatformInfo) => boolean;
6
+ /** Se opera con el dedo: teléfono o tableta. */
7
+ export declare const isHandheld: (p: PlatformInfo) => boolean;
8
+ /** Un cliente que se identificó declara esta capacidad. */
9
+ export declare const hasCapability: (p: PlatformInfo, capability: string) => boolean;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasCapability = exports.isHandheld = exports.isTV = exports.isWebView = void 0;
4
+ /** La página corre dentro de una app y no en un navegador. */
5
+ const isWebView = (p) => p.webview;
6
+ exports.isWebView = isWebView;
7
+ /** Pantalla grande operada con control remoto. */
8
+ const isTV = (p) => p.device === 'tv';
9
+ exports.isTV = isTV;
10
+ /** Se opera con el dedo: teléfono o tableta. */
11
+ const isHandheld = (p) => p.device === 'phone' || p.device === 'tablet';
12
+ exports.isHandheld = isHandheld;
13
+ /** Un cliente que se identificó declara esta capacidad. */
14
+ const hasCapability = (p, capability) => p.capabilities.includes(capability);
15
+ exports.hasCapability = hasCapability;
@@ -0,0 +1,17 @@
1
+ import type { PlatformRegistry, RegistryOptions, UserAgentRule } from './types';
2
+ /**
3
+ * Construye el registro efectivo: los valores del framework más los del proyecto.
4
+ *
5
+ * Todo se valida acá, al construir, y no en la primera petición: un token mal
6
+ * escrito tiene que romper el arranque, no aparecer como una variante que nunca
7
+ * se sirve.
8
+ */
9
+ export declare function createPlatformRegistry(options?: RegistryOptions): PlatformRegistry;
10
+ /**
11
+ * El registro del framework, sin extensiones.
12
+ *
13
+ * Se construye una vez por proceso: nada en él depende del proyecto, así que no
14
+ * hay razón para rearmarlo por cada consumidor.
15
+ */
16
+ export declare const defaultPlatformRegistry: PlatformRegistry;
17
+ export type { UserAgentRule };
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultPlatformRegistry = void 0;
4
+ exports.createPlatformRegistry = createPlatformRegistry;
5
+ const defaults_1 = require("./defaults");
6
+ const user_agent_1 = require("../rules/user-agent");
7
+ /** Un token se escribe en un nombre de archivo, y el punto ya separa tokens. */
8
+ const TOKEN_SHAPE = /^[a-z0-9][a-z0-9-]*$/;
9
+ /**
10
+ * Construye el registro efectivo: los valores del framework más los del proyecto.
11
+ *
12
+ * Todo se valida acá, al construir, y no en la primera petición: un token mal
13
+ * escrito tiene que romper el arranque, no aparecer como una variante que nunca
14
+ * se sirve.
15
+ */
16
+ function createPlatformRegistry(options = {}) {
17
+ const byToken = new Map();
18
+ const byAxis = new Map(defaults_1.ALL_AXES.map((axis) => [axis, []]));
19
+ for (const def of defaults_1.DEFAULT_VALUES)
20
+ register(def, byToken, byAxis, false);
21
+ for (const def of options.values ?? [])
22
+ register(def, byToken, byAxis, true);
23
+ const retiredByToken = new Map(defaults_1.RETIRED_TOKENS.map((def) => [def.token, def]));
24
+ const axisPrecedence = options.axisPrecedence
25
+ ? validatePrecedence(options.axisPrecedence)
26
+ : defaults_1.DEFAULT_AXIS_PRECEDENCE;
27
+ const rules = [...user_agent_1.DEFAULT_RULES, ...(options.detect ?? [])].sort((a, b) => a.priority - b.priority || a.name.localeCompare(b.name));
28
+ for (const [axis, values] of byAxis)
29
+ byAxis.set(axis, values.sort());
30
+ return {
31
+ classify: (token) => byToken.get(token),
32
+ matches(tag, platform) {
33
+ if (tag.axis === 'context')
34
+ return platform.webview === (tag.value === 'webview');
35
+ return String(platform[tag.axis]) === tag.value;
36
+ },
37
+ axisPrecedence,
38
+ values: (axis) => byAxis.get(axis) ?? [],
39
+ has: (axis, value) => byToken.get(value)?.axis === axis,
40
+ retired: (token) => retiredByToken.get(token),
41
+ rules,
42
+ edge: options.edge ?? [],
43
+ };
44
+ }
45
+ /**
46
+ * Añade un valor al índice, validando lo que un dato mal escrito puede romper.
47
+ *
48
+ * `fromProject` distingue los dos orígenes: un choque entre dos valores del
49
+ * framework es un error nuestro y otro mensaje que el de un proyecto intentando
50
+ * redefinir un token que ya existe.
51
+ */
52
+ function register(def, byToken, byAxis, fromProject) {
53
+ if (!TOKEN_SHAPE.test(def.token)) {
54
+ throw new Error(`[ress] Token de plataforma inválido: "${def.token}". ` +
55
+ 'Sólo minúsculas, dígitos y guiones, empezando por letra o dígito. ' +
56
+ 'El punto no puede formar parte de un token porque separa tokens en el sufijo.');
57
+ }
58
+ if (!byAxis.has(def.axis)) {
59
+ throw new Error(`[ress] El token "${def.token}" declara el eje "${def.axis}", que no existe. ` +
60
+ `Ejes válidos: ${defaults_1.ALL_AXES.join(', ')}.`);
61
+ }
62
+ const existing = byToken.get(def.token);
63
+ if (existing) {
64
+ throw new Error(fromProject
65
+ ? `[ress] El token "${def.token}" ya existe en el registro base, en el eje ` +
66
+ `"${existing.axis}". Un proyecto puede agregar valores, no redefinirlos.`
67
+ : `[ress] El token "${def.token}" está declarado dos veces.`);
68
+ }
69
+ byToken.set(def.token, { axis: def.axis, value: def.token });
70
+ byAxis.get(def.axis).push(def.token);
71
+ }
72
+ function validatePrecedence(declared) {
73
+ const unique = new Set(declared);
74
+ const complete = unique.size === declared.length && defaults_1.ALL_AXES.every((a) => unique.has(a));
75
+ if (!complete) {
76
+ throw new Error(`[ress] axisPrecedence debe listar exactamente los ejes ${defaults_1.ALL_AXES.join(', ')}, ` +
77
+ `sin repetir. Se recibió: ${declared.join(', ') || '(vacío)'}.`);
78
+ }
79
+ return declared;
80
+ }
81
+ /**
82
+ * El registro del framework, sin extensiones.
83
+ *
84
+ * Se construye una vez por proceso: nada en él depende del proyecto, así que no
85
+ * hay razón para rearmarlo por cada consumidor.
86
+ */
87
+ exports.defaultPlatformRegistry = createPlatformRegistry();
@@ -0,0 +1,23 @@
1
+ import type { Axis } from '../model/known';
2
+ import type { AxisValueDef, RetiredTokenDef } from './types';
3
+ /**
4
+ * Los doce tokens que el framework conoce.
5
+ *
6
+ * `unknown` no está: es el valor que toma `os` cuando nadie lo resolvió, no un
7
+ * token que alguien pueda escribir en un nombre de archivo.
8
+ */
9
+ export declare const DEFAULT_VALUES: readonly AxisValueDef[];
10
+ /**
11
+ * Tokens que el modelo tuvo y ya no tiene.
12
+ *
13
+ * Un proyecto que los usaba merece un error que explique la migración. Sin esta
14
+ * tabla, `index.mobile.scss` fallaría diciendo apenas que `mobile` no existe.
15
+ */
16
+ export declare const RETIRED_TOKENS: readonly RetiredTokenDef[];
17
+ /**
18
+ * Orden de desempate entre ejes, por selectividad: primero el que menos clientes
19
+ * abarca.
20
+ */
21
+ export declare const DEFAULT_AXIS_PRECEDENCE: readonly Axis[];
22
+ /** Todos los ejes del modelo, para validar una precedencia declarada por un proyecto. */
23
+ export declare const ALL_AXES: readonly Axis[];
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALL_AXES = exports.DEFAULT_AXIS_PRECEDENCE = exports.RETIRED_TOKENS = exports.DEFAULT_VALUES = void 0;
4
+ /**
5
+ * Los doce tokens que el framework conoce.
6
+ *
7
+ * `unknown` no está: es el valor que toma `os` cuando nadie lo resolvió, no un
8
+ * token que alguien pueda escribir en un nombre de archivo.
9
+ */
10
+ exports.DEFAULT_VALUES = [
11
+ // context — la página está incrustada en una app; su ausencia es un navegador
12
+ { token: 'webview', axis: 'context', label: 'Incrustada en una app' },
13
+ // os
14
+ { token: 'ios', axis: 'os', label: 'iOS y iPadOS' },
15
+ { token: 'android', axis: 'os', label: 'Android' },
16
+ { token: 'tizen', axis: 'os', label: 'Samsung Tizen' },
17
+ { token: 'webos', axis: 'os', label: 'LG webOS' },
18
+ { token: 'windows', axis: 'os', label: 'Windows' },
19
+ { token: 'macos', axis: 'os', label: 'macOS' },
20
+ { token: 'linux', axis: 'os', label: 'Linux' },
21
+ // device — forma del aparato y modo de operación
22
+ { token: 'phone', axis: 'device', label: 'Teléfono' },
23
+ { token: 'tablet', axis: 'device', label: 'Tableta' },
24
+ { token: 'tv', axis: 'device', label: 'Pantalla grande, control remoto, foco direccional' },
25
+ { token: 'desktop', axis: 'device', label: 'Escritorio' },
26
+ ];
27
+ /**
28
+ * Tokens que el modelo tuvo y ya no tiene.
29
+ *
30
+ * Un proyecto que los usaba merece un error que explique la migración. Sin esta
31
+ * tabla, `index.mobile.scss` fallaría diciendo apenas que `mobile` no existe.
32
+ */
33
+ exports.RETIRED_TOKENS = [
34
+ {
35
+ token: 'mobile',
36
+ replacement: 'phone o tablet',
37
+ reason: 'era un alias que aparentaba ser un dispositivo. Que dos variantes compartan ' +
38
+ 'estilos se resuelve con un @use común, no con un token de plataforma',
39
+ },
40
+ {
41
+ token: 'web',
42
+ replacement: 'ningún token: la variante base',
43
+ reason: 'era el valor por defecto disfrazado de token. Un WebView también es web',
44
+ },
45
+ {
46
+ token: 'native',
47
+ replacement: 'webview, y los datos de la app llegan en nativeApp',
48
+ reason: 'mezclaba quién contiene la página con qué se renderiza. Si el render es nativo ' +
49
+ 'no hay CSS, así que no puede haber variante de estilos',
50
+ },
51
+ {
52
+ token: 'desktop-app',
53
+ replacement: 'webview.desktop',
54
+ reason: 'una app de escritorio que muestra HTML es una página incrustada en una app',
55
+ },
56
+ ];
57
+ /**
58
+ * Orden de desempate entre ejes, por selectividad: primero el que menos clientes
59
+ * abarca.
60
+ */
61
+ exports.DEFAULT_AXIS_PRECEDENCE = ['context', 'os', 'device'];
62
+ /** Todos los ejes del modelo, para validar una precedencia declarada por un proyecto. */
63
+ exports.ALL_AXES = ['context', 'os', 'device'];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * El registro, por separado.
3
+ *
4
+ * Quien sólo necesita saber qué tokens son válidos —una herramienta de build, un
5
+ * validador de nombres de archivo— importa esto y no arrastra el pipeline de
6
+ * detección.
7
+ */
8
+ export { createPlatformRegistry, defaultPlatformRegistry } from './create';
9
+ export { parseVariantSuffix } from './parse';
10
+ export { ALL_AXES, DEFAULT_AXIS_PRECEDENCE, DEFAULT_VALUES, RETIRED_TOKENS, } from './defaults';
11
+ export type { AxisValueDef, PlatformRegistry, RegistryOptions, RetiredTokenDef, RuleEffect, UserAgentRule, VariantTag, } from './types';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /**
3
+ * El registro, por separado.
4
+ *
5
+ * Quien sólo necesita saber qué tokens son válidos —una herramienta de build, un
6
+ * validador de nombres de archivo— importa esto y no arrastra el pipeline de
7
+ * detección.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.RETIRED_TOKENS = exports.DEFAULT_VALUES = exports.DEFAULT_AXIS_PRECEDENCE = exports.ALL_AXES = exports.parseVariantSuffix = exports.defaultPlatformRegistry = exports.createPlatformRegistry = void 0;
11
+ var create_1 = require("./create");
12
+ Object.defineProperty(exports, "createPlatformRegistry", { enumerable: true, get: function () { return create_1.createPlatformRegistry; } });
13
+ Object.defineProperty(exports, "defaultPlatformRegistry", { enumerable: true, get: function () { return create_1.defaultPlatformRegistry; } });
14
+ var parse_1 = require("./parse");
15
+ Object.defineProperty(exports, "parseVariantSuffix", { enumerable: true, get: function () { return parse_1.parseVariantSuffix; } });
16
+ var defaults_1 = require("./defaults");
17
+ Object.defineProperty(exports, "ALL_AXES", { enumerable: true, get: function () { return defaults_1.ALL_AXES; } });
18
+ Object.defineProperty(exports, "DEFAULT_AXIS_PRECEDENCE", { enumerable: true, get: function () { return defaults_1.DEFAULT_AXIS_PRECEDENCE; } });
19
+ Object.defineProperty(exports, "DEFAULT_VALUES", { enumerable: true, get: function () { return defaults_1.DEFAULT_VALUES; } });
20
+ Object.defineProperty(exports, "RETIRED_TOKENS", { enumerable: true, get: function () { return defaults_1.RETIRED_TOKENS; } });
@@ -0,0 +1,14 @@
1
+ import type { PlatformRegistry, VariantTag } from './types';
2
+ /**
3
+ * Convierte el sufijo de un archivo de variante en tags clasificados.
4
+ *
5
+ * `index.webview.android.scss` tiene el sufijo `webview.android`, que son dos
6
+ * tokens de ejes distintos. El orden en que se escriben no importa: son tags, no
7
+ * una jerarquía, y el resultado se ordena por precedencia para que el sufijo
8
+ * tenga una forma canónica.
9
+ *
10
+ * Un sufijo que el registro no puede clasificar **rompe el arranque**. La
11
+ * alternativa —ignorar el token— produce una variante que existe en el disco y
12
+ * no se sirve nunca, sin ningún aviso.
13
+ */
14
+ export declare function parseVariantSuffix(suffix: string, registry: PlatformRegistry, origin?: string): VariantTag[];
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseVariantSuffix = parseVariantSuffix;
4
+ /**
5
+ * Convierte el sufijo de un archivo de variante en tags clasificados.
6
+ *
7
+ * `index.webview.android.scss` tiene el sufijo `webview.android`, que son dos
8
+ * tokens de ejes distintos. El orden en que se escriben no importa: son tags, no
9
+ * una jerarquía, y el resultado se ordena por precedencia para que el sufijo
10
+ * tenga una forma canónica.
11
+ *
12
+ * Un sufijo que el registro no puede clasificar **rompe el arranque**. La
13
+ * alternativa —ignorar el token— produce una variante que existe en el disco y
14
+ * no se sirve nunca, sin ningún aviso.
15
+ */
16
+ function parseVariantSuffix(suffix, registry, origin) {
17
+ if (suffix === '')
18
+ return [];
19
+ const tags = [];
20
+ const seenAxes = new Map();
21
+ for (const token of suffix.split('.')) {
22
+ const tag = registry.classify(token);
23
+ if (!tag)
24
+ throw unknownToken(token, registry, origin);
25
+ const previous = seenAxes.get(tag.axis);
26
+ if (previous) {
27
+ throw new Error(`[ress] La variante "${suffix}"${where(origin)} pide "${previous}" y "${token}", ` +
28
+ `que son dos valores del eje "${tag.axis}". Una variante no puede dirigirse a ` +
29
+ 'dos valores excluyentes del mismo eje: nunca aplicaría a ningún cliente.');
30
+ }
31
+ seenAxes.set(tag.axis, token);
32
+ tags.push(tag);
33
+ }
34
+ const order = registry.axisPrecedence;
35
+ return tags.sort((a, b) => order.indexOf(a.axis) - order.indexOf(b.axis));
36
+ }
37
+ /**
38
+ * El token no se pudo clasificar. Hay dos motivos posibles y el mensaje es
39
+ * distinto en cada uno: un token retirado se explica con su reemplazo, uno
40
+ * desconocido con la lista de válidos y la sugerencia más cercana.
41
+ */
42
+ function unknownToken(token, registry, origin) {
43
+ const retired = registry.retired(token);
44
+ if (retired) {
45
+ return new Error(`[ress] El token de plataforma "${token}"${where(origin)} ya no existe. ` +
46
+ `Usá ${retired.replacement}.\n` +
47
+ `Se retiró porque ${retired.reason}.`);
48
+ }
49
+ const valid = allTokens(registry);
50
+ const suggestion = closest(token, valid);
51
+ return new Error(`[ress] Token de plataforma desconocido: "${token}"${where(origin)}.` +
52
+ (suggestion ? ` ¿Quisiste decir "${suggestion}"?` : '') +
53
+ `\nTokens válidos: ${valid.join(', ')}.`);
54
+ }
55
+ const where = (origin) => (origin ? ` en "${origin}"` : '');
56
+ function allTokens(registry) {
57
+ return registry.axisPrecedence.flatMap((axis) => [...registry.values(axis)]);
58
+ }
59
+ /**
60
+ * El token válido más parecido, si hay alguno lo bastante cerca.
61
+ *
62
+ * El umbral evita sugerir cualquier cosa ante un error grosero: sugerir `tv`
63
+ * para `televisor` confunde más de lo que ayuda.
64
+ */
65
+ function closest(token, candidates) {
66
+ let best;
67
+ let bestDistance = Math.min(3, Math.ceil(token.length / 2));
68
+ for (const candidate of candidates) {
69
+ const distance = editDistance(token, candidate);
70
+ if (distance <= bestDistance) {
71
+ bestDistance = distance;
72
+ best = candidate;
73
+ }
74
+ }
75
+ return best;
76
+ }
77
+ /** Distancia de Levenshtein, con una sola fila viva. */
78
+ function editDistance(a, b) {
79
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
80
+ for (let i = 1; i <= a.length; i++) {
81
+ const current = [i];
82
+ for (let j = 1; j <= b.length; j++) {
83
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
84
+ current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + cost);
85
+ }
86
+ previous = current;
87
+ }
88
+ return previous[b.length];
89
+ }
@@ -0,0 +1,91 @@
1
+ import type { Axis } from '../model/known';
2
+ import type { Confidence, PlatformInfo } from '../model/types';
3
+ import type { EdgeHintRule } from '../rules/edge';
4
+ /** Un valor válido de un eje. Es un dato, no código. */
5
+ export interface AxisValueDef {
6
+ /** Token tal como se escribe en un nombre de archivo y en una cabecera. */
7
+ token: string;
8
+ axis: Axis;
9
+ /** Para los mensajes de error y la documentación generada. */
10
+ label?: string;
11
+ }
12
+ /**
13
+ * Un token que el modelo tuvo y ya no tiene.
14
+ *
15
+ * Existe para que un proyecto que lo usaba reciba un error que explique la
16
+ * migración, en vez de uno que diga solamente "token desconocido".
17
+ */
18
+ export interface RetiredTokenDef {
19
+ token: string;
20
+ /** Qué usar en su lugar. Aparece textual en el error. */
21
+ replacement: string;
22
+ /** Por qué se retiró. Aparece textual en el error. */
23
+ reason: string;
24
+ }
25
+ /** Un token de un sufijo, ya clasificado. `android` → `{ axis: 'os', value: 'android' }`. */
26
+ export interface VariantTag {
27
+ axis: Axis;
28
+ value: string;
29
+ }
30
+ /** Lo que una regla de User-Agent puede afirmar. */
31
+ export interface RuleEffect {
32
+ webview?: boolean;
33
+ os?: string;
34
+ device?: string;
35
+ }
36
+ /**
37
+ * Una regla de detección por User-Agent.
38
+ *
39
+ * Las reglas son datos: se leen, se auditan y se extienden sin tocar la lógica
40
+ * que las recorre. `priority` define el orden y es normativo — las reglas
41
+ * específicas tienen que evaluarse antes que las genéricas.
42
+ */
43
+ export interface UserAgentRule {
44
+ /** Identificador estable. Aparece en `FieldOrigin.rule`. */
45
+ name: string;
46
+ test: RegExp;
47
+ set: RuleEffect;
48
+ confidence: Confidence;
49
+ priority: number;
50
+ }
51
+ export interface PlatformRegistry {
52
+ /** A qué eje pertenece un token. `undefined` si el registro no lo conoce. */
53
+ classify(token: string): VariantTag | undefined;
54
+ /** ¿Este tag aplica a este cliente? */
55
+ matches(tag: VariantTag, platform: PlatformInfo): boolean;
56
+ /**
57
+ * Orden de desempate entre ejes cuando dos variantes son igual de específicas.
58
+ *
59
+ * El criterio es la selectividad: un eje va antes cuanto menos clientes
60
+ * abarca. `os` selecciona menos aparatos que `device` —iOS son iPhones y
61
+ * iPads, `phone` son todos los teléfonos—, así que va primero.
62
+ */
63
+ readonly axisPrecedence: readonly Axis[];
64
+ /** Valores válidos de un eje, ordenados. */
65
+ values(axis: Axis): readonly string[];
66
+ /** ¿Es un valor conocido de este eje? */
67
+ has(axis: Axis, value: string): boolean;
68
+ /** Si el token fue retirado, su definición. `undefined` si nunca existió. */
69
+ retired(token: string): RetiredTokenDef | undefined;
70
+ /** Reglas de User-Agent efectivas, base más las del proyecto, ya ordenadas. */
71
+ readonly rules: readonly UserAgentRule[];
72
+ /**
73
+ * Cabeceras que la red de distribución del proyecto calcula. Vacío por
74
+ * defecto: qué red hay adelante lo sabe el proyecto, no el framework.
75
+ */
76
+ readonly edge: readonly EdgeHintRule[];
77
+ }
78
+ /** Lo que un proyecto puede agregar al registro base. */
79
+ export interface RegistryOptions {
80
+ /** Valores nuevos. No pueden redefinir un token que ya existe. */
81
+ values?: readonly AxisValueDef[];
82
+ /** Reglas propias. `priority` decide dónde se insertan en el orden. */
83
+ detect?: readonly UserAgentRule[];
84
+ /** Reordena el desempate. Debe listar exactamente los tres ejes. */
85
+ axisPrecedence?: readonly Axis[];
86
+ /**
87
+ * Cabeceras que calcula la red de distribución. `EDGE_PRESETS` trae
88
+ * definiciones listas para las conocidas; ninguna está activa de fábrica.
89
+ */
90
+ edge?: readonly EdgeHintRule[];
91
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,13 @@
1
+ import type { PlatformRegistry } from '../registry/types';
2
+ import type { LayerResult } from './headers';
3
+ /**
4
+ * Recorre la tabla de reglas y devuelve lo que afirmaron, con salida temprana.
5
+ *
6
+ * Las reglas se evalúan en orden y **la primera que fija un campo gana**, igual
7
+ * que entre capas. Cuando los tres ejes ya están resueltos se corta: un
8
+ * User-Agent típico no llega a recorrer la tabla entera.
9
+ *
10
+ * El texto se evalúa crudo, sin pasarlo a minúsculas: las reglas que necesitan
11
+ * distinguir mayúsculas —`AFTB`, `CrKey`— no podrían escribirse de otro modo.
12
+ */
13
+ export declare function readUserAgent(userAgent: string, registry: PlatformRegistry): LayerResult;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readUserAgent = readUserAgent;
4
+ /**
5
+ * Un User-Agent más largo que esto no es un navegador: es ruido o un intento de
6
+ * hacer trabajar de más a las expresiones regulares.
7
+ */
8
+ const MAX_LENGTH = 2048;
9
+ /**
10
+ * Recorre la tabla de reglas y devuelve lo que afirmaron, con salida temprana.
11
+ *
12
+ * Las reglas se evalúan en orden y **la primera que fija un campo gana**, igual
13
+ * que entre capas. Cuando los tres ejes ya están resueltos se corta: un
14
+ * User-Agent típico no llega a recorrer la tabla entera.
15
+ *
16
+ * El texto se evalúa crudo, sin pasarlo a minúsculas: las reglas que necesitan
17
+ * distinguir mayúsculas —`AFTB`, `CrKey`— no podrían escribirse de otro modo.
18
+ */
19
+ function readUserAgent(userAgent, registry) {
20
+ if (!userAgent)
21
+ return {};
22
+ const ua = userAgent.length > MAX_LENGTH ? userAgent.slice(0, MAX_LENGTH) : userAgent;
23
+ const result = {};
24
+ for (const rule of registry.rules) {
25
+ if (result.webview && result.os && result.device)
26
+ break;
27
+ if (!rule.test.test(ua))
28
+ continue;
29
+ if (rule.set.webview !== undefined && !result.webview) {
30
+ result.webview = { value: rule.set.webview, confidence: rule.confidence, rule: rule.name };
31
+ }
32
+ if (rule.set.os !== undefined && !result.os) {
33
+ result.os = { value: rule.set.os, confidence: rule.confidence, rule: rule.name };
34
+ }
35
+ if (rule.set.device !== undefined && !result.device) {
36
+ result.device = { value: rule.set.device, confidence: rule.confidence, rule: rule.name };
37
+ }
38
+ }
39
+ return result;
40
+ }
@@ -0,0 +1,44 @@
1
+ import type { Axis } from '../model/known';
2
+ import type { Confidence } from '../model/types';
3
+ import { type Headers, type LayerResult } from './headers';
4
+ /**
5
+ * Una cabecera que la red de distribución calcula y el origen lee.
6
+ *
7
+ * Es un dato, no código: qué red hay adelante lo sabe el proyecto, no el
8
+ * framework. Traer un proveedor activado de fábrica ataría el framework a él y,
9
+ * peor, haría que todos los demás declaren en `Vary` cabeceras que nadie manda.
10
+ */
11
+ export interface EdgeHintRule {
12
+ /** Nombre de la cabecera, en minúsculas. */
13
+ header: string;
14
+ /** A qué eje aporta. */
15
+ axis: Exclude<Axis, 'context'>;
16
+ /**
17
+ * Cómo se traduce su valor. La clave `'true'` cubre las cabeceras booleanas
18
+ * que algunas redes usan, una por tipo de dispositivo.
19
+ */
20
+ map: Record<string, string>;
21
+ confidence?: Confidence;
22
+ }
23
+ /**
24
+ * Definiciones listas para redes conocidas.
25
+ *
26
+ * Ninguna está activa por defecto: el proyecto declara la suya.
27
+ *
28
+ * ```ts
29
+ * createPlatformRegistry({ edge: EDGE_PRESETS.cloudflare })
30
+ * ```
31
+ *
32
+ * No hace falta un preset para funcionar. Cualquier red que sepa reescribir
33
+ * cabeceras —Workers, Lambda@Edge, VCL, EdgeWorkers— puede setear
34
+ * `x-ressjs-device` directamente, que el framework ya lee sin configurar nada y
35
+ * sin atarse a nadie.
36
+ */
37
+ export declare const EDGE_PRESETS: Record<string, readonly EdgeHintRule[]>;
38
+ /**
39
+ * Lee lo que la red ya resolvió, según las reglas que el proyecto declaró.
40
+ *
41
+ * El orden de las reglas decide: la primera que aporta un eje lo fija, igual que
42
+ * en el resto del pipeline.
43
+ */
44
+ export declare function readEdgeHeaders(headers: Headers, rules: readonly EdgeHintRule[]): LayerResult;