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.
- package/dist/angular.d.ts +2 -2
- package/dist/angular.js +5 -5
- package/dist/auto.js +5 -5
- package/dist/{chunk-W6322KGP.js → chunk-4Z77XNBR.js} +2 -2
- package/dist/{chunk-EMSB2NVX.js → chunk-5JNV45BB.js} +4 -4
- package/dist/{chunk-IH3ZT3QU.js → chunk-C3QOBAUE.js} +1 -1
- package/dist/chunk-DAESKNQ6.js +207 -0
- package/dist/{chunk-BCC25ANK.js → chunk-K6EG7DMO.js} +8 -5
- package/dist/{chunk-6EX5KYT5.js → chunk-KU6HF2EW.js} +2 -2
- package/dist/{chunk-WD4CDGDZ.js → chunk-KZBH5MPD.js} +12 -11
- package/dist/chunk-NZEQGRBL.js +93 -0
- package/dist/chunk-PY7P5BF4.js +404 -0
- package/dist/cli.js +414 -44
- package/dist/dict.d.ts +69 -0
- package/dist/dict.js +8 -0
- package/dist/edge.d.ts +118 -0
- package/dist/edge.js +398 -0
- package/dist/element.d.ts +2 -2
- package/dist/element.js +5 -5
- package/dist/embed.js +2 -2
- package/dist/{events-Dkpkw_Sv.d.ts → events-0rxFlWJJ.d.ts} +1 -1
- package/dist/{expose-OVWTPHH5.js → expose-IAKYHJXF.js} +3 -3
- package/dist/federation.d.ts +1 -1
- package/dist/hydrate-YVZDJ2V7.js +168 -0
- package/dist/iframe.js +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/otel.d.ts +29 -1
- package/dist/otel.js +13 -1
- package/dist/preact.d.ts +4 -4
- package/dist/preact.js +4 -4
- package/dist/prefetch.d.ts +25 -0
- package/dist/prefetch.js +178 -0
- package/dist/react.d.ts +3 -3
- package/dist/react.js +4 -4
- package/dist/reframe-LQL4DZYP.js +251 -0
- package/dist/routes.d.ts +2 -2
- package/dist/routes.js +3 -3
- package/dist/{shadow-PVSETNMZ.js → shadow-S4SKHTA7.js} +2 -2
- package/dist/single-spa.d.ts +1 -1
- package/dist/{slot-UPg6ob9v.d.ts → slot-D49L1rdU.d.ts} +1 -1
- package/dist/solid.d.ts +3 -3
- package/dist/solid.js +4 -4
- package/dist/{sri-M7ZRU57G.js → sri-576GTDIA.js} +2 -1
- package/dist/svelte.d.ts +4 -4
- package/dist/svelte.js +4 -4
- package/dist/testing.d.ts +171 -3
- package/dist/testing.js +191 -4
- package/dist/{types-CA2p9KHC.d.ts → types-DUtcF3HO.d.ts} +12 -1
- package/dist/vitals.d.ts +29 -0
- package/dist/vitals.js +152 -0
- package/dist/vite.js +28 -422
- package/dist/vue.d.ts +2 -2
- package/dist/vue.js +4 -4
- 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-
|
|
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
|
-
*
|
|
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
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/vitals.d.ts
ADDED
|
@@ -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
|
+
};
|