vitral 0.4.0 → 0.5.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 (55) hide show
  1. package/dist/angular.d.ts +2 -2
  2. package/dist/angular.js +5 -5
  3. package/dist/auto.js +5 -5
  4. package/dist/{chunk-W6322KGP.js → chunk-4Z77XNBR.js} +2 -2
  5. package/dist/{chunk-EMSB2NVX.js → chunk-5JNV45BB.js} +4 -4
  6. package/dist/{chunk-IH3ZT3QU.js → chunk-C3QOBAUE.js} +1 -1
  7. package/dist/chunk-DAESKNQ6.js +207 -0
  8. package/dist/{chunk-BCC25ANK.js → chunk-K6EG7DMO.js} +8 -5
  9. package/dist/{chunk-6EX5KYT5.js → chunk-KU6HF2EW.js} +2 -2
  10. package/dist/{chunk-WD4CDGDZ.js → chunk-KZBH5MPD.js} +12 -11
  11. package/dist/chunk-NZEQGRBL.js +93 -0
  12. package/dist/chunk-PY7P5BF4.js +404 -0
  13. package/dist/cli.js +414 -44
  14. package/dist/dict.d.ts +69 -0
  15. package/dist/dict.js +8 -0
  16. package/dist/edge.d.ts +118 -0
  17. package/dist/edge.js +398 -0
  18. package/dist/element.d.ts +2 -2
  19. package/dist/element.js +5 -5
  20. package/dist/embed.js +2 -2
  21. package/dist/{events-Dkpkw_Sv.d.ts → events-0rxFlWJJ.d.ts} +1 -1
  22. package/dist/{expose-OVWTPHH5.js → expose-IAKYHJXF.js} +3 -3
  23. package/dist/federation.d.ts +1 -1
  24. package/dist/hydrate-YVZDJ2V7.js +168 -0
  25. package/dist/iframe.js +4 -4
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +3 -3
  28. package/dist/otel.d.ts +29 -1
  29. package/dist/otel.js +13 -1
  30. package/dist/preact.d.ts +4 -4
  31. package/dist/preact.js +4 -4
  32. package/dist/prefetch.d.ts +25 -0
  33. package/dist/prefetch.js +178 -0
  34. package/dist/react.d.ts +3 -3
  35. package/dist/react.js +4 -4
  36. package/dist/reframe-LQL4DZYP.js +251 -0
  37. package/dist/routes.d.ts +2 -2
  38. package/dist/routes.js +3 -3
  39. package/dist/{shadow-PVSETNMZ.js → shadow-S4SKHTA7.js} +2 -2
  40. package/dist/single-spa.d.ts +1 -1
  41. package/dist/{slot-UPg6ob9v.d.ts → slot-D49L1rdU.d.ts} +1 -1
  42. package/dist/solid.d.ts +3 -3
  43. package/dist/solid.js +4 -4
  44. package/dist/{sri-M7ZRU57G.js → sri-576GTDIA.js} +2 -1
  45. package/dist/svelte.d.ts +4 -4
  46. package/dist/svelte.js +4 -4
  47. package/dist/testing.d.ts +171 -3
  48. package/dist/testing.js +191 -4
  49. package/dist/{types-CA2p9KHC.d.ts → types-DUtcF3HO.d.ts} +12 -1
  50. package/dist/vitals.d.ts +29 -0
  51. package/dist/vitals.js +152 -0
  52. package/dist/vite.js +28 -422
  53. package/dist/vue.d.ts +2 -2
  54. package/dist/vue.js +4 -4
  55. package/package.json +107 -16
package/dist/testing.d.ts CHANGED
@@ -1,9 +1,177 @@
1
- import { P as Props, R as RemoteContext } from './types-CA2p9KHC.js';
1
+ import { P as Props, R as RemoteContext, a as RemoteModule } from './types-DUtcF3HO.js';
2
2
  export { r as resetVitral } from './state-BDdCwc21.js';
3
3
  import './errors-kjBv6IH_.js';
4
4
 
5
+ /** Endereço do JSON Schema do contrato. Vai no `$schema` do arquivo gravado. */
6
+ declare const CONTRACT_SCHEMA = "https://joaogabriel15.github.io/vitral/schema/vitral-contract.json";
5
7
  /**
6
- * Utilidades de teste: monte o host sem subir nenhum remote de verdade.
8
+ * Contrato do consumidor: o que um host passou para um remote e o que ele
9
+ * espera de volta, gravado com `record()`. Arquivo `<remote>.contract.json`.
10
+ */
11
+ interface Contract {
12
+ $schema?: string;
13
+ /** Nome com que o host monta o remote. */
14
+ remote: string;
15
+ /** Quem gravou (o host). */
16
+ host?: string;
17
+ /** `version` do manifesto do remote na gravação. */
18
+ version?: string;
19
+ /** Uma montagem por caso, sem repetições. */
20
+ cases: ContractCase[];
21
+ }
22
+ /** Quando o remote emitiu: no `mount`, num `update`, ao receber um evento do host, ou depois (clique, timer). */
23
+ type EmitPhase = 'mount' | 'update' | 'input' | 'later';
24
+ /**
25
+ * Uma montagem gravada. Valores que o JSON não guarda viram marcadores:
26
+ * `{ "$fn": true }` (função), `{ "$date": "…" }` e `{ "$unknown": "Classe" }`.
27
+ */
28
+ interface ContractCase {
29
+ /** Props do mount. */
30
+ props: Props;
31
+ /** Props de cada `update`, em ordem. */
32
+ updates?: Props[];
33
+ /** Eventos que o host emitiu e o remote escutava, em ordem. */
34
+ inputs?: {
35
+ type: string;
36
+ payload?: unknown;
37
+ }[];
38
+ /** Valores que o remote leu com `ctx.events.last`. */
39
+ last?: Record<string, unknown>;
40
+ /** Chaves de `ctx.shared` que o remote leu, com o primeiro valor lido. */
41
+ shared?: Record<string, unknown>;
42
+ /** Eventos que o remote emitiu e o host escuta. */
43
+ emits?: {
44
+ type: string;
45
+ phase: EmitPhase;
46
+ }[];
47
+ /** Eventos que o remote assinou. */
48
+ listens?: string[];
49
+ }
50
+ interface HarnessOptions<P extends Props = Props> {
51
+ props: P;
52
+ /** Nome do remote em `ctx.name`. Padrão `'remote'`. */
53
+ name?: string;
54
+ /** Monta dentro de um shadow root (`ctx.shadowRoot` e `ctx.portalContainer` ficam nele). */
55
+ shadow?: boolean;
56
+ /** Valores iniciais de `ctx.shared`. */
57
+ shared?: Record<string, unknown>;
58
+ /** Valores iniciais de `ctx.events.last`. */
59
+ last?: Record<string, unknown>;
60
+ /** Campos que substituem os do `ctx` simulado. */
61
+ ctx?: Partial<RemoteContext>;
62
+ /** Onde montar. Padrão: um `<div>` novo no `document.body`. */
63
+ target?: HTMLElement;
64
+ }
65
+ interface Harness<P extends Props = Props> {
66
+ /** Elemento onde o remote montou. */
67
+ readonly el: HTMLElement;
68
+ /** O `ctx` que o remote recebeu. */
69
+ readonly ctx: RemoteContext;
70
+ /** Eventos que o remote emitiu por `ctx.events`, em ordem. */
71
+ readonly events: {
72
+ type: string;
73
+ payload: unknown;
74
+ }[];
75
+ /** Erros de `ctx.reportError` e de handlers de eventos. */
76
+ readonly errors: unknown[];
77
+ /** Chamadas de `ctx.navigate`. */
78
+ readonly navigations: {
79
+ url: string;
80
+ options?: {
81
+ replace?: boolean;
82
+ };
83
+ }[];
84
+ /** Tipos de evento que o remote escuta agora. */
85
+ listens(): string[];
86
+ /** Emite um evento como se fosse o host. */
87
+ emit(type: string, payload?: unknown): void;
88
+ update(props: P): void;
89
+ /** Dispara `ctx.onActivate` (keep-alive). */
90
+ activate(): void;
91
+ /** Dispara `ctx.onDeactivate` (keep-alive). */
92
+ deactivate(): void;
93
+ unmount(): void;
94
+ }
95
+ /**
96
+ * Monta `mod` com um `ctx` simulado: barramento de eventos próprio, `shared`
97
+ * em memória, `portalContainer`, `customElements`, `navigate` e
98
+ * `reportError` gravados.
99
+ *
100
+ * const h = await harness(remote, { props: { itens: 2 } })
101
+ * expect(h.el.textContent).toContain('2 itens')
102
+ * h.emit('tema', 'escuro')
103
+ * expect(h.events).toContainEqual({ type: 'carrinho:aberto', payload: undefined })
104
+ */
105
+ declare function harness<P extends Props = Props>(mod: RemoteModule<P> | {
106
+ default: RemoteModule<P>;
107
+ }, options: HarnessOptions<P>): Promise<Harness<P>>;
108
+ interface VerifyOptions {
109
+ /**
110
+ * Código do remote (o texto dos arquivos JS). Um evento que o host escuta e
111
+ * que o remote só emite depois (clique, timer) é procurado aqui pelo nome.
112
+ */
113
+ sources?: string[];
114
+ /** Quanto esperar depois de cada etapa, em ms. Padrão 0. */
115
+ wait?: number;
116
+ }
117
+ interface VerifyResult {
118
+ /** Casos reproduzidos. */
119
+ cases: number;
120
+ /** O que quebra o host. Vazio quando o contrato passa. */
121
+ failures: string[];
122
+ /** O que mudou sem quebrar. */
123
+ warnings: string[];
124
+ }
125
+ /**
126
+ * Reproduz cada caso de `contract` em `mod` (props, updates, eventos do host,
127
+ * `shared` e `last`) e confere: mount, update e unmount sem erro, nenhum
128
+ * `reportError`, e cada evento que o host escuta ainda emitido (ou, se só sai
129
+ * depois de um clique, ainda presente no código).
130
+ */
131
+ declare function verify(contract: Contract, mod: RemoteModule<any> | {
132
+ default: RemoteModule<any>;
133
+ }, options?: VerifyOptions): Promise<VerifyResult>;
134
+
135
+ /**
136
+ * Gravação do contrato do consumidor, no host: para cada remote montado,
137
+ * guarda as props, os updates, os eventos que o host emitiu para ele, o que
138
+ * ele leu de `ctx.shared` e `ctx.events.last`, e os eventos dele que o host
139
+ * escuta. O remote reproduz o arquivo no CI com `vitral test --contract`.
140
+ *
141
+ * Funciona trocando métodos do protocolo global (`load` e o barramento), sem
142
+ * nada no núcleo: o host só paga por isto quando importa `vitral/testing`.
143
+ */
144
+
145
+ interface RecordOptions {
146
+ /** Nome do host, gravado no contrato. */
147
+ host?: string;
148
+ /** Máximo de casos diferentes por remote. Padrão 50. */
149
+ max?: number;
150
+ }
151
+ interface Recorder {
152
+ /** Contratos gravados até agora, por nome do remote. */
153
+ contracts(): Record<string, Contract>;
154
+ /** Grava `<remote>.contract.json` em `dir` (só no Node). Devolve os caminhos. */
155
+ save(dir?: string): Promise<string[]>;
156
+ /** Para de gravar e devolve o protocolo como estava. */
157
+ stop(): void;
158
+ }
159
+ /**
160
+ * Começa a gravar. Chame antes de o host montar os remotes e de assinar
161
+ * eventos (o recorder só vê as assinaturas feitas depois dele).
162
+ *
163
+ * const rec = record({ host: 'loja' })
164
+ * // ...testes do host montam os remotes...
165
+ * await rec.save('contracts')
166
+ *
167
+ * Remotes em `isolate: 'iframe'` e partes (`carrinho/Mini`) não são gravados.
168
+ */
169
+ declare function record(options?: RecordOptions): Recorder;
170
+
171
+ /**
172
+ * Utilidades de teste: monte o host sem subir nenhum remote de verdade, monte
173
+ * um remote com `ctx` simulado (`harness`) e grave o contrato do consumidor
174
+ * (`record`) que o remote reproduz no CI com `vitral test --contract`.
7
175
  *
8
176
  * import { mockRemote } from 'vitral/testing'
9
177
  * const carrinho = mockRemote('carrinho', (el, props) => (el.textContent = `itens: ${props.itens}`))
@@ -26,4 +194,4 @@ declare function mockRemote<P extends Props = Props>(name: string, render?: (el:
26
194
  /** Remote que sempre falha ao montar, para testar telas de erro. */
27
195
  declare function failingRemote(name: string, message?: string): void;
28
196
 
29
- export { type MockRemote, failingRemote, mockRemote };
197
+ export { CONTRACT_SCHEMA, type Contract, type ContractCase, type EmitPhase, type Harness, type HarnessOptions, type MockRemote, type RecordOptions, type Recorder, type VerifyOptions, type VerifyResult, failingRemote, harness, mockRemote, record, verify };
package/dist/testing.js CHANGED
@@ -1,14 +1,197 @@
1
- import "./chunk-WD4CDGDZ.js";
1
+ import {
2
+ CONTRACT_SCHEMA,
3
+ encode,
4
+ harness,
5
+ verify
6
+ } from "./chunk-DAESKNQ6.js";
7
+ import "./chunk-KZBH5MPD.js";
2
8
  import {
3
9
  register
4
- } from "./chunk-EMSB2NVX.js";
5
- import "./chunk-6EX5KYT5.js";
10
+ } from "./chunk-5JNV45BB.js";
11
+ import "./chunk-KU6HF2EW.js";
6
12
  import "./chunk-TW7KYJQ6.js";
7
13
  import "./chunk-463H6B56.js";
8
14
  import {
15
+ protocol,
9
16
  resetProtocol
10
17
  } from "./chunk-GNPFWISJ.js";
11
18
 
19
+ // src/recorder.ts
20
+ function record(options = {}) {
21
+ const p = protocol();
22
+ const bus = p.events;
23
+ const { load, emit, on } = { load: p.load, emit: bus.emit, on: bus.on };
24
+ const cases = /* @__PURE__ */ new Map();
25
+ const versions = /* @__PURE__ */ new Map();
26
+ const hostListens = /* @__PURE__ */ new Set();
27
+ const wrapped = /* @__PURE__ */ new WeakMap();
28
+ let viaRemote = 0;
29
+ let hostEmitting = [];
30
+ bus.on = (type, handler) => {
31
+ if (viaRemote) return on.call(bus, type, handler);
32
+ hostListens.add(type);
33
+ return on.call(bus, type, (payload) => {
34
+ const before = viaRemote;
35
+ viaRemote = 0;
36
+ try {
37
+ handler(payload);
38
+ } finally {
39
+ viaRemote = before;
40
+ }
41
+ });
42
+ };
43
+ bus.emit = (type, payload) => {
44
+ hostEmitting.push(!viaRemote);
45
+ try {
46
+ return emit.call(bus, type, payload);
47
+ } finally {
48
+ hostEmitting.pop();
49
+ }
50
+ };
51
+ const scope = (live, events) => ({
52
+ emit(type, payload) {
53
+ live.emits.push({ type, phase: live.phase });
54
+ viaRemote++;
55
+ try {
56
+ events.emit(type, payload);
57
+ } finally {
58
+ viaRemote--;
59
+ }
60
+ },
61
+ on(type, handler) {
62
+ live.listens.add(type);
63
+ viaRemote++;
64
+ try {
65
+ return events.on(type, (payload) => {
66
+ if (!hostEmitting.at(-1)) return handler(payload);
67
+ live.inputs.push({ type, payload: encode(payload) });
68
+ const before = live.phase;
69
+ live.phase = "input";
70
+ try {
71
+ handler(payload);
72
+ } finally {
73
+ live.phase = before;
74
+ }
75
+ });
76
+ } finally {
77
+ viaRemote--;
78
+ }
79
+ },
80
+ last(type) {
81
+ const v = events.last(type);
82
+ if (v !== void 0 && !(type in live.last)) live.last[type] = encode(v);
83
+ return v;
84
+ }
85
+ });
86
+ const sharedOf = (live, api) => {
87
+ const seen = (key, v) => {
88
+ if (v !== void 0 && !(key in live.shared)) live.shared[key] = encode(v);
89
+ return v;
90
+ };
91
+ const wrap = (key) => {
92
+ const h = api(key);
93
+ return { ...h, get: () => seen(key, h.get()), subscribe: (fn) => h.subscribe((v) => fn(seen(key, v))) };
94
+ };
95
+ return Object.assign(wrap, { provide: api.provide, batch: api.batch });
96
+ };
97
+ const wrapModule = (mod) => {
98
+ let w = wrapped.get(mod);
99
+ if (w) return w;
100
+ w = Object.create(mod, {
101
+ mount: {
102
+ value: async (el, props, ctx) => {
103
+ const live = { props: encode(props), updates: [], inputs: [], last: {}, shared: {}, emits: [], listens: /* @__PURE__ */ new Set(), phase: "mount" };
104
+ const list = cases.get(ctx.name) ?? [];
105
+ cases.set(ctx.name, list);
106
+ list.push(live);
107
+ const own = { ...ctx, events: scope(live, ctx.events) };
108
+ if (ctx.shared) own.shared = sharedOf(live, ctx.shared);
109
+ let mounted;
110
+ try {
111
+ mounted = await mod.mount(el, props, own);
112
+ } finally {
113
+ live.phase = "later";
114
+ }
115
+ return {
116
+ update(next) {
117
+ live.updates.push(encode(next));
118
+ live.phase = "update";
119
+ try {
120
+ mounted.update?.(next);
121
+ } finally {
122
+ live.phase = "later";
123
+ }
124
+ },
125
+ unmount: () => mounted.unmount()
126
+ };
127
+ }
128
+ }
129
+ });
130
+ wrapped.set(mod, w);
131
+ return w;
132
+ };
133
+ p.load = (name, fetcher) => load.call(p, name, fetcher).then((r) => {
134
+ if (r.version) versions.set(r.name ?? name, r.version);
135
+ return typeof r.module?.mount === "function" ? { ...r, module: wrapModule(r.module) } : r;
136
+ });
137
+ const contracts = () => {
138
+ const out = {};
139
+ for (const [remote, lives] of cases) {
140
+ const unique = /* @__PURE__ */ new Map();
141
+ for (const live of lives) {
142
+ const c = { props: live.props };
143
+ if (live.updates.length) c.updates = live.updates;
144
+ if (live.inputs.length) c.inputs = live.inputs;
145
+ if (Object.keys(live.last).length) c.last = live.last;
146
+ if (Object.keys(live.shared).length) c.shared = live.shared;
147
+ const emits = /* @__PURE__ */ new Map();
148
+ for (const e of live.emits) if (hostListens.has(e.type) && (!emits.has(e.type) || emits.get(e.type) === "later")) emits.set(e.type, e.phase);
149
+ if (emits.size) c.emits = [...emits].map(([type, phase]) => ({ type, phase }));
150
+ if (live.listens.size) c.listens = [...live.listens].sort();
151
+ const key = JSON.stringify(c);
152
+ if (!unique.has(key) && unique.size < (options.max ?? 50)) unique.set(key, c);
153
+ }
154
+ out[remote] = {
155
+ $schema: CONTRACT_SCHEMA,
156
+ remote,
157
+ ...options.host && { host: options.host },
158
+ ...versions.has(remote) && { version: versions.get(remote) },
159
+ cases: [...unique.values()]
160
+ };
161
+ }
162
+ return out;
163
+ };
164
+ return {
165
+ contracts,
166
+ async save(dir = ".") {
167
+ const fs = "node:fs/promises";
168
+ const path = "node:path";
169
+ const { mkdir, writeFile } = await import(
170
+ /* @vite-ignore */
171
+ fs
172
+ );
173
+ const { join } = await import(
174
+ /* @vite-ignore */
175
+ path
176
+ );
177
+ await mkdir(dir, { recursive: true });
178
+ const files = [];
179
+ for (const [name, c] of Object.entries(contracts())) {
180
+ const file = join(dir, `${name.replace(/[^\w.-]+/g, "_")}.contract.json`);
181
+ await writeFile(file, JSON.stringify(c, null, 2) + "\n");
182
+ files.push(file);
183
+ }
184
+ return files;
185
+ },
186
+ stop() {
187
+ p.load = load;
188
+ bus.emit = emit;
189
+ bus.on = on;
190
+ hostEmitting = [];
191
+ }
192
+ };
193
+ }
194
+
12
195
  // src/testing.ts
13
196
  function mockRemote(name, render = (el) => {
14
197
  el.setAttribute("data-mock-remote", name);
@@ -47,7 +230,11 @@ function failingRemote(name, message = "falha simulada") {
47
230
  });
48
231
  }
49
232
  export {
233
+ CONTRACT_SCHEMA,
50
234
  failingRemote,
235
+ harness,
51
236
  mockRemote,
52
- resetProtocol as resetVitral
237
+ record,
238
+ resetProtocol as resetVitral,
239
+ verify
53
240
  };
@@ -157,6 +157,15 @@ interface MountedRemote<P extends Props = Props> {
157
157
  }
158
158
  interface RemoteModule<P extends Props = Props> {
159
159
  mount(el: HTMLElement, props: P, ctx: RemoteContext): MountedRemote<P> | Promise<MountedRemote<P>>;
160
+ /**
161
+ * Opcional. Quando o gateway de `vitral/edge` já renderizou o remote no
162
+ * servidor, o `<vitral-remote>` chama `hydrate` com `el` cheio do HTML de
163
+ * `render` (no Shadow DOM declarativo, se for o caso), em vez de limpar e
164
+ * chamar `mount`. Deve assumir o DOM que existe, sem recriar.
165
+ */
166
+ hydrate?(el: HTMLElement, props: P, ctx: RemoteContext): MountedRemote<P> | Promise<MountedRemote<P>>;
167
+ /** Opcional, só no servidor: o HTML do remote para estas props (SSR pelo gateway). */
168
+ render?(props: P): string | Promise<string>;
160
169
  }
161
170
  interface RemoteEntry {
162
171
  /** URL do módulo JS do remote, ou do `vitral.json` gerado pelo plugin. */
@@ -243,8 +252,10 @@ interface RollbackInfo {
243
252
  /**
244
253
  * `none`: CSS com escopo. `shadow`: Shadow DOM. `iframe`: iframe com
245
254
  * `sandbox`, para código de terceiros (precisa de `enableIframe()`).
255
+ * `reframe`: JS num iframe oculto de mesma origem (globals próprios) e DOM
256
+ * no Shadow DOM da página. Não é fronteira de segurança.
246
257
  */
247
- type Isolation = 'shadow' | 'none' | 'iframe';
258
+ type Isolation = 'shadow' | 'none' | 'iframe' | 'reframe';
248
259
  /** Estilos do host dentro do Shadow DOM. Sem efeito no modo escopo. */
249
260
  interface StyleOptions {
250
261
  /**
@@ -0,0 +1,29 @@
1
+ type VitalName = 'LCP' | 'CLS' | 'INP' | 'FCP' | 'TTFB';
2
+ type VitalRating = 'good' | 'needs-improvement' | 'poor';
3
+ interface VitalReport {
4
+ name: VitalName;
5
+ /** Remote dono do elemento. Ausente quando é do host ou da página. */
6
+ remote?: string;
7
+ /** Versão do remote (do manifesto) na hora da medição. */
8
+ version?: string;
9
+ /** Rota: pathname da navegação (soft ou não). */
10
+ route: string;
11
+ /** `navigationId` das entradas, só com Soft Navigations. */
12
+ navigationId?: string;
13
+ /** ms para LCP, INP, FCP e TTFB; sem unidade para CLS. */
14
+ value: number;
15
+ rating: VitalRating;
16
+ }
17
+ interface VitalsOptions {
18
+ onReport: (report: VitalReport) => void;
19
+ }
20
+ declare const rating: (name: VitalName, value: number) => VitalRating;
21
+ /** Dono de `node`: o `data-vitral-root` mais próximo, atravessando shadow roots. `''` é o host. */
22
+ declare function owner(node: Node | null | undefined): string;
23
+ /**
24
+ * Observa as Web Vitals e chama `onReport` com cada uma, por remote e rota.
25
+ * Devolve `stop`, que relata o que falta e desliga os observers.
26
+ */
27
+ declare function vitals({ onReport }: VitalsOptions): () => void;
28
+
29
+ export { type VitalName, type VitalRating, type VitalReport, type VitalsOptions, owner, rating, vitals };
package/dist/vitals.js ADDED
@@ -0,0 +1,152 @@
1
+ import {
2
+ mounted
3
+ } from "./chunk-463H6B56.js";
4
+ import "./chunk-GNPFWISJ.js";
5
+
6
+ // src/vitals.ts
7
+ var LIMITS = {
8
+ LCP: [2500, 4e3],
9
+ CLS: [0.1, 0.25],
10
+ INP: [200, 500],
11
+ FCP: [1800, 3e3],
12
+ TTFB: [800, 1800]
13
+ };
14
+ var rating = (name, value) => value <= LIMITS[name][0] ? "good" : value <= LIMITS[name][1] ? "needs-improvement" : "poor";
15
+ function owner(node) {
16
+ while (node) {
17
+ const el = node.nodeType === 1 ? node : node.parentElement;
18
+ const root = el?.closest("[data-vitral-root]");
19
+ if (root) return root.getAttribute("data-vitral-root");
20
+ node = (el ?? node).getRootNode().host;
21
+ }
22
+ return "";
23
+ }
24
+ function vitals({ onReport }) {
25
+ if (typeof PerformanceObserver == "undefined") return () => {
26
+ };
27
+ const types = PerformanceObserver.supportedEntryTypes ?? [];
28
+ const soft = types.includes("soft-navigation");
29
+ const navs = /* @__PURE__ */ new Map();
30
+ const done = /* @__PURE__ */ new Set();
31
+ const routes = /* @__PURE__ */ new Map();
32
+ const versions = /* @__PURE__ */ new Map();
33
+ const observers = [];
34
+ const send = (name, value, nav, who2 = "") => {
35
+ const key = name + "|" + who2;
36
+ if (nav.sent.get(key) === value) return;
37
+ nav.sent.set(key, value);
38
+ const report = { name, route: routes.get(nav.id)?.[0] ?? nav.route, value, rating: rating(name, value) };
39
+ if (who2) report.remote = who2, report.version = versions.get(who2);
40
+ if (nav.id !== void 0) report.navigationId = nav.id;
41
+ if (report.version === void 0) delete report.version;
42
+ try {
43
+ onReport(report);
44
+ } catch {
45
+ }
46
+ };
47
+ const flush = (nav) => {
48
+ const start = routes.get(nav.id)?.[1] ?? nav.start;
49
+ if (nav.lcp) send("LCP", Math.max(0, nav.lcp[0] - start), nav, nav.lcp[1]);
50
+ for (const [who2, s] of nav.cls) send("CLS", s.max, nav, who2);
51
+ for (const [who2, list] of nav.inp) {
52
+ const all2 = [...list.values()].sort((a, b) => b - a);
53
+ send("INP", all2[Math.min(all2.length - 1, Math.floor(all2.length / 50))], nav, who2);
54
+ }
55
+ };
56
+ const navOf = (e) => {
57
+ const id = soft && e.navigationId != null ? String(e.navigationId) : void 0;
58
+ const key = id ?? "r:" + location.pathname;
59
+ if (done.has(key)) return;
60
+ let nav = navs.get(key);
61
+ if (!nav) {
62
+ for (const [k, old] of navs) flush(old), navs.delete(k), old.id !== void 0 && done.add(k);
63
+ navs.set(key, nav = { id, route: location.pathname, start: 0, cls: /* @__PURE__ */ new Map(), inp: /* @__PURE__ */ new Map(), sent: /* @__PURE__ */ new Map() });
64
+ }
65
+ return nav;
66
+ };
67
+ const who = (node) => {
68
+ const name = owner(node);
69
+ if (name && !versions.has(name)) versions.set(name, mounted.get(name)?.version);
70
+ return name;
71
+ };
72
+ const handle = (e) => {
73
+ switch (e.entryType) {
74
+ case "soft-navigation": {
75
+ routes.set(String(e.navigationId), [new URL(e.name, location.href).pathname, e.startTime]);
76
+ return;
77
+ }
78
+ case "navigation": {
79
+ if (e.navigationId != null && soft) routes.set(String(e.navigationId), [new URL(e.name, location.href).pathname, 0]);
80
+ const nav = navOf(e);
81
+ if (nav && e.responseStart > 0) send("TTFB", Math.max(0, e.responseStart - (e.activationStart ?? 0)), nav);
82
+ return;
83
+ }
84
+ case "paint": {
85
+ const nav = e.name == "first-contentful-paint" && navOf(e);
86
+ if (nav) send("FCP", Math.max(0, e.startTime - (activation() ?? 0)), nav);
87
+ return;
88
+ }
89
+ case "largest-contentful-paint": {
90
+ const nav = navOf(e);
91
+ if (nav) nav.lcp = [e.startTime, who(e.element)];
92
+ return;
93
+ }
94
+ case "layout-shift": {
95
+ if (e.hadRecentInput) return;
96
+ const nav = navOf(e);
97
+ if (!nav) return;
98
+ let big;
99
+ let area = -1;
100
+ for (const s2 of e.sources ?? []) {
101
+ const r = s2.currentRect ?? s2.previousRect;
102
+ const a = r ? r.width * r.height : 0;
103
+ if (a > area) area = a, big = s2.node;
104
+ }
105
+ const name = who(big);
106
+ const t = e.startTime;
107
+ const s = nav.cls.get(name);
108
+ if (s && t - s.last < 1e3 && t - s.first < 5e3) s.current += e.value;
109
+ else if (s) s.current = e.value, s.first = t;
110
+ if (s) s.last = t, s.max = Math.max(s.max, s.current);
111
+ else nav.cls.set(name, { first: t, last: t, current: e.value, max: e.value });
112
+ return;
113
+ }
114
+ case "event": {
115
+ if (!e.interactionId) return;
116
+ const nav = navOf(e);
117
+ if (!nav) return;
118
+ const name = who(e.target);
119
+ const list = nav.inp.get(name) ?? /* @__PURE__ */ new Map();
120
+ nav.inp.set(name, list);
121
+ list.set(e.interactionId, Math.max(list.get(e.interactionId) ?? 0, e.duration));
122
+ }
123
+ }
124
+ };
125
+ const activation = () => performance.getEntriesByType?.("navigation")[0]?.activationStart;
126
+ for (const type of ["soft-navigation", "navigation", "paint", "largest-contentful-paint", "layout-shift", "event"]) {
127
+ if (!types.includes(type)) continue;
128
+ try {
129
+ const o = new PerformanceObserver((list) => list.getEntries().forEach((e) => handle(e)));
130
+ o.observe({ type, buffered: true, durationThreshold: 40, includeSoftNavigationObservations: true });
131
+ observers.push(o);
132
+ } catch {
133
+ }
134
+ }
135
+ const all = () => {
136
+ for (const o of observers) o.takeRecords().forEach((e) => handle(e));
137
+ navs.forEach(flush);
138
+ };
139
+ const hidden = () => document.visibilityState == "hidden" && all();
140
+ document.addEventListener("visibilitychange", hidden);
141
+ return () => {
142
+ document.removeEventListener("visibilitychange", hidden);
143
+ all();
144
+ observers.forEach((o) => o.disconnect());
145
+ observers.length = 0;
146
+ };
147
+ }
148
+ export {
149
+ owner,
150
+ rating,
151
+ vitals
152
+ };