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/cli.js CHANGED
@@ -1,7 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ verify
4
+ } from "./chunk-DAESKNQ6.js";
5
+ import {
6
+ loadTs
7
+ } from "./chunk-PY7P5BF4.js";
8
+ import {
9
+ dictMatch
10
+ } from "./chunk-NZEQGRBL.js";
2
11
 
3
12
  // src/cli.ts
4
- import { createRequire } from "module";
13
+ import { createRequire as createRequire2 } from "module";
5
14
 
6
15
  // src/cli/canal.ts
7
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -420,10 +429,100 @@ function serveMcp(input = process.stdin, output = process.stdout, options = {})
420
429
  return new Promise((done) => lines.on("close", () => Promise.all(pending).then(() => done())));
421
430
  }
422
431
 
432
+ // src/cli/dict.ts
433
+ import { execFileSync } from "child_process";
434
+ import { createHash, randomBytes } from "crypto";
435
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync4 } from "fs";
436
+ import { join, relative, sep } from "path";
437
+ import * as zlib from "zlib";
438
+ var INDEX = "vitral-dict.json";
439
+ var TYPES = { ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css" };
440
+ var DCZ_MAGIC = Buffer.from([94, 42, 77, 24, 32, 0, 0, 0]);
441
+ var DCB_MAGIC = Buffer.from([255, 68, 67, 66]);
442
+ function files(dist) {
443
+ const out = /* @__PURE__ */ new Map();
444
+ const walk = (dir) => {
445
+ for (const name of readdirSync(dir).sort()) {
446
+ const full = join(dir, name);
447
+ if (statSync(full).isDirectory()) walk(full);
448
+ else if (TYPES[ext(name)]) out.set(relative(dist, full).split(sep).join("/"), full);
449
+ }
450
+ };
451
+ walk(dist);
452
+ return out;
453
+ }
454
+ var ext = (name) => name.slice(name.lastIndexOf("."));
455
+ function nodeZstd() {
456
+ const z = zlib.zstdCompressSync;
457
+ if (!z) return void 0;
458
+ const sample = randomBytes(256);
459
+ return z(sample, { dictionary: sample }).length < 64 ? z : void 0;
460
+ }
461
+ function brotliBin() {
462
+ try {
463
+ execFileSync("brotli", ["--version"], { stdio: "ignore" });
464
+ } catch {
465
+ return void 0;
466
+ }
467
+ return (input, dictionary) => execFileSync("brotli", ["-q", "11", "-c", "-D", dictionary, input], { maxBuffer: 1 << 30 });
468
+ }
469
+ var kb = (n) => (n / 1024).toFixed(1).padStart(7) + " kB";
470
+ function dict(o) {
471
+ const log = o.log ?? console.log;
472
+ const zstd = nodeZstd();
473
+ if (!zstd) throw new Error(`[vitral] dict precisa do zstd com dicion\xE1rio do Node (22.19+ ou 24.6+). Este \xE9 ${process.version}.`);
474
+ const brotli = o.brotli === false ? void 0 : o.brotli ?? brotliBin();
475
+ const level = o.level ?? 19;
476
+ if (!existsSync4(o.dist)) throw new Error(`[vitral] ${o.dist} n\xE3o existe`);
477
+ const novos = files(o.dist);
478
+ const antigos = /* @__PURE__ */ new Map();
479
+ for (const from of o.from) {
480
+ if (!existsSync4(from)) throw new Error(`[vitral] ${from} n\xE3o existe`);
481
+ for (const [rel2, full] of files(from)) {
482
+ const key = dictMatch("/" + rel2);
483
+ antigos.set(key, [...antigos.get(key) ?? [], full]);
484
+ }
485
+ }
486
+ const index = { version: 1, files: {} };
487
+ const linhas = [];
488
+ for (const [rel2, full] of novos) {
489
+ const deltas = {};
490
+ index.files[rel2] = { type: TYPES[ext(rel2)], deltas };
491
+ const input = readFileSync4(full);
492
+ const vistos = /* @__PURE__ */ new Set();
493
+ for (const old of antigos.get(dictMatch("/" + rel2)) ?? []) {
494
+ const dictionary = readFileSync4(old);
495
+ const sha = createHash("sha256").update(dictionary).digest();
496
+ const hash = `:${sha.toString("base64")}:`;
497
+ if (vistos.has(hash) || dictionary.equals(input)) continue;
498
+ vistos.add(hash);
499
+ const id = sha.toString("hex").slice(0, 12);
500
+ const variant = {};
501
+ const dcz = Buffer.concat([DCZ_MAGIC, sha, zstd(input, { dictionary, params: { [zlib.constants.ZSTD_c_compressionLevel]: level } })]);
502
+ writeFileSync4(join(o.dist, `${rel2}.${id}.dcz`), dcz);
503
+ variant.dcz = `${rel2}.${id}.dcz`;
504
+ let dcb;
505
+ if (brotli) {
506
+ dcb = Buffer.concat([DCB_MAGIC, sha, brotli(full, old)]);
507
+ writeFileSync4(join(o.dist, `${rel2}.${id}.dcb`), dcb);
508
+ variant.dcb = `${rel2}.${id}.dcb`;
509
+ }
510
+ deltas[hash] = variant;
511
+ const br = zlib.brotliCompressSync(input, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 } }).length;
512
+ linhas.push(`${rel2} br ${kb(br)} dcz ${kb(dcz.length)}${dcb ? ` dcb ${kb(dcb.length)}` : ""} (de ${relative(process.cwd(), old) || old})`);
513
+ }
514
+ }
515
+ writeFileSync4(join(o.dist, INDEX), JSON.stringify(index, null, 2) + "\n");
516
+ const n = Object.values(index.files).reduce((s, f) => s + Object.keys(f.deltas).length, 0);
517
+ for (const l of linhas) log(l);
518
+ log(`[vitral] ${Object.keys(index.files).length} arquivos, ${n} deltas${brotli ? "" : " (s\xF3 .dcz: falta o bin\xE1rio brotli para .dcb)"}. \xCDndice: ${join(o.dist, INDEX)}`);
519
+ return index;
520
+ }
521
+
423
522
  // src/cli/doctor.ts
424
- import { createHash } from "crypto";
425
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
426
- import { dirname as dirname2, join, resolve as resolve3 } from "path";
523
+ import { createHash as createHash2 } from "crypto";
524
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
525
+ import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
427
526
  import { gzipSync } from "zlib";
428
527
  var WARN_KB = 250;
429
528
  var ANY_ORIGIN = "https://host.invalid";
@@ -431,7 +530,7 @@ var satisfies;
431
530
  var ok = (title) => ({ status: "ok", title });
432
531
  var warn = (title, cause, fix) => ({ status: "warn", title, cause, fix });
433
532
  var error = (title, cause, fix) => ({ status: "error", title, cause, fix });
434
- var kb = (bytes) => `${(bytes / 1024).toFixed(1)} kB`;
533
+ var kb2 = (bytes) => `${(bytes / 1024).toFixed(1)} kB`;
435
534
  async function get(f, url, origin, timeout) {
436
535
  try {
437
536
  const res = await f(url, { headers: { Origin: origin }, signal: AbortSignal.timeout(timeout) });
@@ -503,8 +602,8 @@ async function hostPolicies(f, cwd, host, timeout, out) {
503
602
  for (const h of (res.headers.get("content-security-policy") ?? "").split(",")) if (h.trim()) policies.push(parseCsp(h, `header de ${host}`));
504
603
  for (const c of meta(new TextDecoder().decode(bytes))) policies.push(parseCsp(c, `<meta> de ${host}`));
505
604
  } else {
506
- const file = ["index.html", "public/index.html", "src/index.html"].map((p) => resolve3(cwd, p)).find((p) => existsSync4(p));
507
- if (file) for (const c of meta(readFileSync4(file, "utf8"))) policies.push(parseCsp(c, `<meta> de ${file}`));
605
+ const file = ["index.html", "public/index.html", "src/index.html"].map((p) => resolve3(cwd, p)).find((p) => existsSync5(p));
606
+ if (file) for (const c of meta(readFileSync5(file, "utf8"))) policies.push(parseCsp(c, `<meta> de ${file}`));
508
607
  }
509
608
  out.push(
510
609
  policies.length ? ok(`CSP encontrada (${policies.map((p) => p.source).join(", ")})`) : ok(`CSP: nenhuma encontrada ${host ? `em ${host}` : "no index.html"}${host ? "" : " (passe --host para ler o header da p\xE1gina)"}`)
@@ -552,7 +651,7 @@ function matchesIntegrity(bytes, integrity) {
552
651
  if (!hashes.length) return void 0;
553
652
  const bits = Math.max(...hashes.map((m) => +m[1]));
554
653
  const clean2 = (b) => b.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
555
- const actual = clean2(createHash(`sha${bits}`).update(bytes).digest("base64"));
654
+ const actual = clean2(createHash2(`sha${bits}`).update(bytes).digest("base64"));
556
655
  return hashes.some((m) => +m[1] === bits && clean2(m[2]) === actual);
557
656
  }
558
657
  var FRAMEWORKS = {
@@ -561,8 +660,8 @@ var FRAMEWORKS = {
561
660
  };
562
661
  function installed(cwd, pkg) {
563
662
  for (let dir = resolve3(cwd); ; dir = dirname2(dir)) {
564
- const file = join(dir, "node_modules", pkg, "package.json");
565
- if (existsSync4(file)) return JSON.parse(readFileSync4(file, "utf8")).version;
663
+ const file = join2(dir, "node_modules", pkg, "package.json");
664
+ if (existsSync5(file)) return JSON.parse(readFileSync5(file, "utf8")).version;
566
665
  if (dir === dirname2(dir)) return;
567
666
  }
568
667
  }
@@ -655,7 +754,7 @@ async function checkRemote(name, e, ctx, seen) {
655
754
  }
656
755
  if (!e.integrity && !m.integrity) out.push(warn("sem integrity: o JS roda sem confer\xEAncia", void 0, "gere o manifesto com o plugin vitral/vite ou fixe integrity no host."));
657
756
  const gz = gzipSync(bytes).length;
658
- const size = `tamanho do JS: ${kb(bytes.length)} (${kb(gz)} com gzip)`;
757
+ const size = `tamanho do JS: ${kb2(bytes.length)} (${kb2(gz)} com gzip)`;
659
758
  if (ctx.maxKb !== void 0 && gz > ctx.maxKb * 1024)
660
759
  out.push(error(size, `passa do limite de ${ctx.maxKb} kB.`, "veja o que entrou no bundle (vite build --mode production com rollup-plugin-visualizer) e use dedupe para o framework."));
661
760
  else if (ctx.maxKb === void 0 && gz > WARN_KB * 1024) out.push(warn(size, `acima de ${WARN_KB} kB com gzip, o remote atrasa a p\xE1gina.`, "confira o que entrou no bundle; use dedupe para o framework; passe --max-kb para tornar isto um erro."));
@@ -672,9 +771,9 @@ async function checkRemote(name, e, ctx, seen) {
672
771
  );
673
772
  }
674
773
  }
675
- const files = Object.entries(m.files ?? {}).filter(([f]) => abs(f) !== js);
774
+ const files2 = Object.entries(m.files ?? {}).filter(([f]) => abs(f) !== js);
676
775
  const bad = [];
677
- for (const [file, integrity] of files) {
776
+ for (const [file, integrity] of files2) {
678
777
  const got = await get(ctx.f, abs(file), ctx.origin, ctx.timeout);
679
778
  if (!got.res?.ok) bad.push(`${file} (${got.failure ?? `HTTP ${got.res.status}`})`);
680
779
  else if (!matchesIntegrity(got.bytes, integrity)) bad.push(`${file} (hash n\xE3o confere)`);
@@ -685,7 +784,7 @@ async function checkRemote(name, e, ctx, seen) {
685
784
  }
686
785
  if (bad.length)
687
786
  out.push(error(`arquivos do manifesto com problema: ${bad.join(", ")}`, "o host n\xE3o carrega o CSS ou o build dividido (INTEGRITY ou LOAD_FAILED).", "publique todos os arquivos do mesmo build junto com o vitral.json."));
688
- else if (files.length) out.push(ok(`${files.length} arquivo(s) do manifesto conferem com o hash`));
787
+ else if (files2.length) out.push(ok(`${files2.length} arquivo(s) do manifesto conferem com o hash`));
689
788
  const range = e.contract ?? ctx.contract;
690
789
  if (m.contract?.version && range) {
691
790
  if (satisfies(m.contract.version, range)) out.push(ok(`contrato ${m.contract.version} na faixa ${range} do host`));
@@ -804,8 +903,8 @@ async function doctorCli(argv, options = {}, log = console.log) {
804
903
  }
805
904
 
806
905
  // src/cli/migrate.ts
807
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
808
- import { basename, dirname as dirname3, extname, join as join2, relative, resolve as resolve4 } from "path";
906
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
907
+ import { basename, dirname as dirname3, extname, join as join3, relative as relative2, resolve as resolve4 } from "path";
809
908
  var FRAMEWORKS2 = {
810
909
  preact: { wrap: ["vitral/preact", "preactRemote"], plugin: ["@preact/preset-vite", "preact", "preact()"] },
811
910
  react: { wrap: ["vitral/react", "reactRemote"], plugin: ["@vitejs/plugin-react", "react", "react()"] },
@@ -831,13 +930,13 @@ var VITE_CONFIG = (ts) => `vite.vitral.config.${ts ? "mts" : "mjs"}`;
831
930
  var OUT_DIR = "dist-vitral";
832
931
  function read(file) {
833
932
  try {
834
- return readFileSync5(file, "utf8");
933
+ return readFileSync6(file, "utf8");
835
934
  } catch {
836
935
  return null;
837
936
  }
838
937
  }
839
938
  function rel(from, to) {
840
- const r = relative(from, to).replace(/\\/g, "/");
939
+ const r = relative2(from, to).replace(/\\/g, "/");
841
940
  return r.startsWith(".") ? r : "./" + r;
842
941
  }
843
942
  function spec(from, to) {
@@ -868,28 +967,28 @@ function pairs(obj) {
868
967
  return [...obj.matchAll(re)].map((m) => [m[2], m[4] ?? m[6]]);
869
968
  }
870
969
  function project(cwd) {
871
- const raw = read(join2(cwd, "package.json"));
970
+ const raw = read(join3(cwd, "package.json"));
872
971
  const pkg = raw ? JSON.parse(raw) : null;
873
972
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies, ...pkg?.peerDependencies };
874
- return { cwd, pkg, deps, ts: existsSync5(join2(cwd, "tsconfig.json")), framework: DETECT.find(([d]) => d in deps)?.[1] ?? null };
973
+ return { cwd, pkg, deps, ts: existsSync6(join3(cwd, "tsconfig.json")), framework: DETECT.find(([d]) => d in deps)?.[1] ?? null };
875
974
  }
876
975
  function sources2(dir, out = []) {
877
976
  let list;
878
977
  try {
879
- list = readdirSync(dir, { withFileTypes: true });
978
+ list = readdirSync2(dir, { withFileTypes: true });
880
979
  } catch {
881
980
  return out;
882
981
  }
883
982
  for (const e of list) {
884
983
  if (e.isDirectory()) {
885
- if (!/^(node_modules|dist|build|\..*)$/.test(e.name)) sources2(join2(dir, e.name), out);
886
- } else if (/\.(tsx?|jsx?|mjs)$/.test(e.name) && !/\.(test|spec|d)\./.test(e.name)) out.push(join2(dir, e.name));
984
+ if (!/^(node_modules|dist|build|\..*)$/.test(e.name)) sources2(join3(dir, e.name), out);
985
+ } else if (/\.(tsx?|jsx?|mjs)$/.test(e.name) && !/\.(test|spec|d)\./.test(e.name)) out.push(join3(dir, e.name));
887
986
  }
888
987
  return out;
889
988
  }
890
989
  var LIFECYCLE_EXPORT = /export\s+(?:(?:async\s+)?function\s*\*?\s*mount\b|(?:const|let|var)\s+mount\b|(?:const|let|var)\s*\{[^}]*\bmount\b[^}]*\}|\{[^}]*\bmount\b[^}]*\})/;
891
990
  function configs(cwd) {
892
- return CONFIGS.map((f) => ({ file: f, text: read(join2(cwd, f)) })).filter((c) => c.text !== null);
991
+ return CONFIGS.map((f) => ({ file: f, text: read(join3(cwd, f)) })).filter((c) => c.text !== null);
893
992
  }
894
993
  function detect(p) {
895
994
  if (configs(p.cwd).some((c) => MF_CALL.test(c.text) && /\bexposes\s*:/.test(c.text))) return "module-federation";
@@ -899,7 +998,7 @@ function detect(p) {
899
998
  }
900
999
  function viteConfig(p, vitral, notes) {
901
1000
  const path = VITE_CONFIG(p.ts);
902
- if (existsSync5(join2(p.cwd, path))) {
1001
+ if (existsSync6(join3(p.cwd, path))) {
903
1002
  notes.push(`${path} j\xE1 existe e n\xE3o foi alterado. Confira se ele usa: ${vitral}`);
904
1003
  return null;
905
1004
  }
@@ -922,16 +1021,16 @@ function viteConfig(p, vitral, notes) {
922
1021
  }
923
1022
  function buildScript(p) {
924
1023
  if (!p.pkg || p.pkg.scripts?.["build:vitral"]) return null;
925
- const before = read(join2(p.cwd, "package.json"));
1024
+ const before = read(join3(p.cwd, "package.json"));
926
1025
  const indent = /^(\s+)"/m.exec(before)?.[1] ?? " ";
927
1026
  const pkg = { ...p.pkg, scripts: { ...p.pkg.scripts, "build:vitral": `vite build -c ${VITE_CONFIG(p.ts)}` } };
928
1027
  return { path: "package.json", before, after: JSON.stringify(pkg, null, indent) + (before.endsWith("\n") ? "\n" : "") };
929
1028
  }
930
1029
  function resolveSource(base, file) {
931
1030
  const abs = resolve4(base, file);
932
- if (extname(abs) && existsSync5(abs)) return abs;
933
- for (const ext of SOURCE_EXT) if (existsSync5(abs + ext)) return abs + ext;
934
- for (const ext of SOURCE_EXT) if (existsSync5(join2(abs, "index" + ext))) return join2(abs, "index" + ext);
1031
+ if (extname(abs) && existsSync6(abs)) return abs;
1032
+ for (const ext2 of SOURCE_EXT) if (existsSync6(abs + ext2)) return abs + ext2;
1033
+ for (const ext2 of SOURCE_EXT) if (existsSync6(join3(abs, "index" + ext2))) return join3(abs, "index" + ext2);
935
1034
  return abs;
936
1035
  }
937
1036
  function reactRoot(text2, file) {
@@ -950,13 +1049,13 @@ function reactRoot(text2, file) {
950
1049
  function singleSpa(p) {
951
1050
  const changes = [];
952
1051
  const notes = [];
953
- const src = existsSync5(join2(p.cwd, "src")) ? join2(p.cwd, "src") : p.cwd;
954
- const files = sources2(src);
955
- const found = files.filter((f) => {
1052
+ const src = existsSync6(join3(p.cwd, "src")) ? join3(p.cwd, "src") : p.cwd;
1053
+ const files2 = sources2(src);
1054
+ const found = files2.filter((f) => {
956
1055
  const t = read(f);
957
1056
  return LIFECYCLE_EXPORT.test(t) && /\bunmount\b/.test(t);
958
1057
  });
959
- const roots = files.filter((f) => /\bregisterApplication\s*\(/.test(read(f)));
1058
+ const roots = files2.filter((f) => /\bregisterApplication\s*\(/.test(read(f)));
960
1059
  if (roots.length) {
961
1060
  notes.push(
962
1061
  `Root config em ${roots.map((f) => rel(p.cwd, f)).join(", ")}: troque cada registerApplication por configure({ remotes }) e o activeWhen por uma rota do host ou routes() de vitral/routes. Veja https://joaogabriel15.github.io/vitral/migracao/single-spa`
@@ -968,7 +1067,7 @@ function singleSpa(p) {
968
1067
  }
969
1068
  const [file, ...others] = found;
970
1069
  if (others.length) notes.push(`Outros arquivos tamb\xE9m exportam lifecycles e n\xE3o foram migrados: ${others.map((f) => rel(p.cwd, f)).join(", ")}`);
971
- const out = join2(src, `vitral-remote.${p.ts ? "ts" : "js"}`);
1070
+ const out = join3(src, `vitral-remote.${p.ts ? "ts" : "js"}`);
972
1071
  const outRel = rel(p.cwd, out).replace(/^\.\//, "");
973
1072
  const text2 = read(file);
974
1073
  const root = /single-spa-react/.test(text2) ? reactRoot(text2, file) : null;
@@ -991,13 +1090,13 @@ export default singleSpaRemote(lifecycles)
991
1090
  `${outRel} embrulha os lifecycles de ${rel(p.cwd, file)} sem mudar o app. Para tirar o single-spa do bundle, troque por reactRemote, vueRemote etc. Veja https://joaogabriel15.github.io/vitral/migracao/single-spa`
992
1091
  );
993
1092
  }
994
- if (existsSync5(out)) notes.push(`${outRel} j\xE1 existe e n\xE3o foi alterado.`);
1093
+ if (existsSync6(out)) notes.push(`${outRel} j\xE1 existe e n\xE3o foi alterado.`);
995
1094
  else changes.push({ path: outRel, before: null, after: code });
996
1095
  const vite = viteConfig(p, `vitral({ entry: '${outRel}' })`, notes);
997
1096
  if (vite) changes.push(vite);
998
1097
  const script = buildScript(p);
999
1098
  if (script) changes.push(script);
1000
- if (/\b(navigateToUrl|mountParcel|mountRootParcel)\b/.test(files.map((f) => read(f)).join("\n"))) {
1099
+ if (/\b(navigateToUrl|mountParcel|mountRootParcel)\b/.test(files2.map((f) => read(f)).join("\n"))) {
1001
1100
  notes.push("O c\xF3digo usa navigateToUrl ou parcels do single-spa: fora de um host single-spa, troque navigateToUrl por ctx.navigate e parcels por <Remote> ou createSlot.");
1002
1101
  }
1003
1102
  if (/vite-plugin-single-spa|webpack-config-single-spa|systemjs/.test(Object.keys(p.deps).join(" "))) {
@@ -1041,8 +1140,8 @@ ${sugestoes.join("\n")}
1041
1140
  return { from: "module-federation", changes, notes };
1042
1141
  }
1043
1142
  const name = /\bname\s*:\s*(['"`])([^'"`]+)\1/.exec(remote.call)?.[2];
1044
- const base = join2(p.cwd, dirname3(remote.file));
1045
- const wrapDir = join2(p.cwd, "src", "vitral");
1143
+ const base = join3(p.cwd, dirname3(remote.file));
1144
+ const wrapDir = join3(p.cwd, "src", "vitral");
1046
1145
  const fw = p.framework && p.framework !== "angular" ? FRAMEWORKS2[p.framework] : null;
1047
1146
  const entries = [];
1048
1147
  let entry;
@@ -1057,7 +1156,7 @@ ${sugestoes.join("\n")}
1057
1156
  modules.push(key);
1058
1157
  continue;
1059
1158
  }
1060
- const wrapper = join2(wrapDir, `${key.slice(2).replace(/[^\w-]/g, "-")}.${p.ts ? "ts" : "js"}`);
1159
+ const wrapper = join3(wrapDir, `${key.slice(2).replace(/[^\w-]/g, "-")}.${p.ts ? "ts" : "js"}`);
1061
1160
  const wrapperRel = rel(p.cwd, wrapper).replace(/^\.\//, "");
1062
1161
  const imp = spec(wrapDir, file);
1063
1162
  const [from, fn] = fw.wrap;
@@ -1067,7 +1166,7 @@ import Componente from '${imp}'
1067
1166
  export * from '${imp}'
1068
1167
  export default ${fn}(Componente)
1069
1168
  `;
1070
- if (existsSync5(wrapper)) notes.push(`${wrapperRel} j\xE1 existe e n\xE3o foi alterado.`);
1169
+ if (existsSync6(wrapper)) notes.push(`${wrapperRel} j\xE1 existe e n\xE3o foi alterado.`);
1071
1170
  else changes.push({ path: wrapperRel, before: null, after: code });
1072
1171
  entries.push([key, wrapperRel]);
1073
1172
  entry ??= wrapperRel;
@@ -1141,15 +1240,259 @@ ${result.notes.map((n) => ` - ${n}`).join("\n")}`);
1141
1240
  return result;
1142
1241
  }
1143
1242
  for (const c of result.changes) {
1144
- const file = join2(cwd, c.path);
1243
+ const file = join3(cwd, c.path);
1145
1244
  mkdirSync2(dirname3(file), { recursive: true });
1146
- writeFileSync4(file, c.after);
1245
+ writeFileSync5(file, c.after);
1147
1246
  }
1148
1247
  if (result.changes.length) log(`
1149
1248
  [vitral] ${result.changes.length} arquivo(s) escrito(s). Rode \`npm run build:vitral\`.`);
1150
1249
  return result;
1151
1250
  }
1152
1251
 
1252
+ // src/cli/test.ts
1253
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
1254
+ import { createRequire } from "module";
1255
+ import { dirname as dirname4, join as join4, resolve as resolve5 } from "path";
1256
+ import { pathToFileURL } from "url";
1257
+ var FILE = "/__vitral_check__.ts";
1258
+ function typeErrors(ts, snippets) {
1259
+ let code = "";
1260
+ const starts = [];
1261
+ snippets.forEach((s, i) => {
1262
+ starts.push(code.length);
1263
+ code += `namespace __c${i} {
1264
+ ${s}
1265
+ }
1266
+ `;
1267
+ });
1268
+ const options = { strict: true, noEmit: true, target: ts.ScriptTarget.ES2022, skipLibCheck: true, types: [] };
1269
+ const host = ts.createCompilerHost(options);
1270
+ const getSourceFile = host.getSourceFile;
1271
+ host.getSourceFile = (name, lang, ...rest) => name === FILE ? ts.createSourceFile(name, code, lang) : getSourceFile.call(host, name, lang, ...rest);
1272
+ const fileExists = host.fileExists;
1273
+ host.fileExists = (name) => name === FILE || fileExists.call(host, name);
1274
+ const program = ts.createProgram([FILE], options, host);
1275
+ const out = snippets.map(() => []);
1276
+ for (const d of [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()]) {
1277
+ if (d.file?.fileName !== FILE || d.start === void 0) continue;
1278
+ let i = starts.length - 1;
1279
+ while (i > 0 && starts[i] > d.start) i--;
1280
+ out[i].push({ code: d.code, text: ts.flattenDiagnosticMessageText(d.messageText, " ") });
1281
+ }
1282
+ return out;
1283
+ }
1284
+ function propFields(ts, type) {
1285
+ const src = ts.createSourceFile("p.ts", `type P = ${type}`, ts.ScriptTarget.ES2022, true);
1286
+ const alias = src.statements[0];
1287
+ if (!alias || !ts.isTypeAliasDeclaration(alias) || !ts.isTypeLiteralNode(alias.type)) return null;
1288
+ const fields = /* @__PURE__ */ new Map();
1289
+ for (const m of alias.type.members) {
1290
+ if (!ts.isPropertySignature(m) || !m.type) return null;
1291
+ const name = ts.isIdentifier(m.name) || ts.isStringLiteral(m.name) ? m.name.text : m.name.getText(src);
1292
+ fields.set(name, { optional: !!m.questionToken, type: m.type.getText(src) });
1293
+ }
1294
+ return fields;
1295
+ }
1296
+ function diffProps(before, after, ts) {
1297
+ const breaking = [];
1298
+ const compatible = [];
1299
+ const norm = (s) => s.replace(/\s+/g, " ").replace(/;\s*}/g, " }").trim();
1300
+ if (norm(before) === norm(after)) return { breaking, compatible };
1301
+ if (!ts) return { breaking: [`Props mudou (sem TypeScript para detalhar): ${norm(before)} \u2192 ${norm(after)}`], compatible };
1302
+ const old = propFields(ts, before);
1303
+ const now = propFields(ts, after);
1304
+ if (!old || !now) {
1305
+ const [errors] = typeErrors(ts, [`type O = ${before}
1306
+ type N = ${after}
1307
+ declare const o: O
1308
+ export const n: N = o`]);
1309
+ (errors.length ? breaking : compatible).push(`Props mudou: ${norm(before)} \u2192 ${norm(after)}`);
1310
+ return { breaking, compatible };
1311
+ }
1312
+ const changed = [];
1313
+ for (const [name, f] of now) {
1314
+ const was = old.get(name);
1315
+ if (!was) (f.optional ? compatible : breaking).push(`${f.optional ? "prop opcional nova" : "prop obrigat\xF3ria nova"}: ${name}: ${f.type}`);
1316
+ else {
1317
+ if (was.optional && !f.optional) breaking.push(`${name} virou obrigat\xF3ria`);
1318
+ if (!was.optional && f.optional) compatible.push(`${name} virou opcional`);
1319
+ if (norm(was.type) !== norm(f.type)) changed.push([name, was, f]);
1320
+ }
1321
+ }
1322
+ for (const [name, f] of old) if (!now.has(name)) breaking.push(`prop removida: ${name}: ${f.type} (o host que ainda passa ${name} n\xE3o compila)`);
1323
+ const results = typeErrors(
1324
+ ts,
1325
+ changed.map(([, was, f]) => `type O = ${was.type}
1326
+ type N = ${f.type}
1327
+ declare const o: O
1328
+ export const n: N = o`)
1329
+ );
1330
+ changed.forEach(([name, was, f], i) => {
1331
+ if (results[i].length) breaking.push(`tipo de ${name} mudou: ${norm(was.type)} \u2192 ${norm(f.type)} (${results[i][0].text})`);
1332
+ else compatible.push(`tipo de ${name} aceita mais valores: ${norm(was.type)} \u2192 ${norm(f.type)}`);
1333
+ });
1334
+ return { breaking, compatible };
1335
+ }
1336
+ function tsLiteral(value) {
1337
+ if (value === null || typeof value !== "object") return value === void 0 ? "undefined" : JSON.stringify(value);
1338
+ if (Array.isArray(value)) return `[${value.map(tsLiteral).join(", ")}]`;
1339
+ const v = value;
1340
+ if (v.$fn === true) return "((..._a: any[]): any => undefined)";
1341
+ if (typeof v.$date === "string") return `new Date(${JSON.stringify(v.$date)})`;
1342
+ if (typeof v.$unknown === "string") return "(undefined as any)";
1343
+ return `{ ${Object.entries(v).map(([k, x]) => `${JSON.stringify(k)}: ${tsLiteral(x)}`).join(", ")} }`;
1344
+ }
1345
+ var EXCESS = /* @__PURE__ */ new Set([2353, 2561]);
1346
+ function checkCaseProps(contract, props, ts) {
1347
+ const failures = [];
1348
+ const warnings = [];
1349
+ const fields = propFields(ts, props);
1350
+ const all = [];
1351
+ contract.cases.forEach((c, i) => {
1352
+ all.push({ at: `caso ${i + 1}`, value: c.props });
1353
+ c.updates?.forEach((u, j) => all.push({ at: `caso ${i + 1}, update ${j + 1}`, value: u }));
1354
+ });
1355
+ const extra = (at, what) => warnings.push(`${at}: o host passa uma prop que o remote n\xE3o declara: ${what}`);
1356
+ const values = all.map(({ at, value }) => {
1357
+ if (!fields || !value || typeof value !== "object") return value;
1358
+ const known = Object.entries(value).filter(([k]) => fields.has(k) || (extra(at, k), false));
1359
+ return Object.fromEntries(known);
1360
+ });
1361
+ const results = typeErrors(
1362
+ ts,
1363
+ values.map((value) => `type Props = ${props}
1364
+ export const p: Props = ${tsLiteral(value)}`)
1365
+ );
1366
+ results.forEach((errors, i) => {
1367
+ for (const e of errors) {
1368
+ if (EXCESS.has(e.code)) extra(all[i].at, e.text);
1369
+ else failures.push(`${all[i].at}: props n\xE3o batem com o Props local: ${e.text}`);
1370
+ }
1371
+ });
1372
+ return { failures, warnings };
1373
+ }
1374
+ async function installDom(cwd) {
1375
+ let mod;
1376
+ try {
1377
+ mod = await import(pathToFileURL(createRequire(join4(cwd, "package.json")).resolve("happy-dom")).href);
1378
+ } catch {
1379
+ try {
1380
+ mod = await import("happy-dom");
1381
+ } catch {
1382
+ throw new Error("[vitral] vitral test precisa do happy-dom: npm i -D happy-dom");
1383
+ }
1384
+ }
1385
+ const win = new mod.Window({ url: "http://localhost/" });
1386
+ const g = globalThis;
1387
+ const dom = /* @__PURE__ */ new Set(["Event", "EventTarget", "CustomEvent", "MessageEvent", "DOMException", "navigator", "window", "self"]);
1388
+ const before = /* @__PURE__ */ new Map();
1389
+ for (const key of Object.getOwnPropertyNames(win)) {
1390
+ if (key in g && !dom.has(key)) continue;
1391
+ const desc = Object.getOwnPropertyDescriptor(g, key);
1392
+ if (desc && !desc.configurable) continue;
1393
+ before.set(key, desc);
1394
+ Object.defineProperty(g, key, { value: win[key], configurable: true, writable: true });
1395
+ }
1396
+ return async () => {
1397
+ await win.happyDOM?.close?.();
1398
+ for (const [key, desc] of before) {
1399
+ if (desc) Object.defineProperty(g, key, desc);
1400
+ else delete g[key];
1401
+ }
1402
+ };
1403
+ }
1404
+ function contractFiles(cwd, paths) {
1405
+ return paths.flatMap((p) => {
1406
+ const abs = resolve5(cwd, p);
1407
+ if (!existsSync7(abs)) throw new Error(`[vitral] contrato n\xE3o encontrado: ${p}`);
1408
+ return statSync2(abs).isDirectory() ? readdirSync3(abs).filter((f) => f.endsWith(".contract.json")).sort().map((f) => join4(abs, f)) : [abs];
1409
+ });
1410
+ }
1411
+ function sourcesIn(dir) {
1412
+ const out = [];
1413
+ const walk = (d) => {
1414
+ for (const f of readdirSync3(d, { withFileTypes: true })) {
1415
+ if (f.name === "node_modules") continue;
1416
+ const p = join4(d, f.name);
1417
+ if (f.isDirectory()) walk(p);
1418
+ else if (/\.[cm]?js$/.test(f.name)) out.push(readFileSync7(p, "utf8"));
1419
+ }
1420
+ };
1421
+ walk(dir);
1422
+ return out;
1423
+ }
1424
+ function publishedTypes(since, cwd) {
1425
+ if (/^https?:\/\//.test(since)) {
1426
+ if (since.endsWith(".d.ts")) return since;
1427
+ return new URL("vitral.d.ts", since.endsWith(".json") || since.endsWith("/") ? since : since + "/").href;
1428
+ }
1429
+ const abs = resolve5(cwd, since);
1430
+ return existsSync7(abs) && statSync2(abs).isDirectory() ? join4(abs, "vitral.d.ts") : abs;
1431
+ }
1432
+ async function readText(where, get2) {
1433
+ if (!/^https?:\/\//.test(where)) return readFileSync7(where, "utf8");
1434
+ const res = await get2(where);
1435
+ if (!res.ok) throw new Error(`HTTP ${res.status} em ${where}`);
1436
+ return res.text();
1437
+ }
1438
+ async function test(options = {}) {
1439
+ const cwd = options.cwd ?? process.cwd();
1440
+ const log = options.log ?? console.log;
1441
+ const get2 = options.fetch ?? fetch;
1442
+ const dir = resolve5(cwd, options.dir ?? "dist");
1443
+ if (!options.contracts?.length && !options.since) throw new Error("[vitral] passe --contract arquivo e/ou --since url. Veja `vitral --help`.");
1444
+ const localFile = resolve5(cwd, options.types ?? join4(dir, "vitral.d.ts"));
1445
+ const local = existsSync7(localFile) ? parseProps(readFileSync7(localFile, "utf8")) : null;
1446
+ let ts;
1447
+ const typescript = async () => ts === void 0 ? ts = await loadTs(cwd) : ts;
1448
+ let failures = 0;
1449
+ if (options.since) {
1450
+ const where = publishedTypes(options.since, cwd);
1451
+ if (!local) throw new Error(`[vitral] sem Props local em ${localFile}. Rode o build com o plugin, ou passe --types.`);
1452
+ const before = parseProps(await readText(where, get2));
1453
+ if (!before) throw new Error(`[vitral] ${where} sem "export type Props".`);
1454
+ const diff2 = diffProps(before, local, await typescript());
1455
+ for (const c of diff2.breaking) log(`\u2717 quebra: ${c}`);
1456
+ for (const c of diff2.compatible) log(`\u2713 compat\xEDvel: ${c}`);
1457
+ log(diff2.breaking.length ? `[vitral] ${diff2.breaking.length} mudan\xE7a(s) nas props que quebram o host (desde ${where}).` : `[vitral] props compat\xEDveis com ${where}.`);
1458
+ failures += diff2.breaking.length;
1459
+ }
1460
+ if (options.contracts?.length) {
1461
+ const files2 = contractFiles(cwd, options.contracts);
1462
+ let entry = options.entry && resolve5(cwd, options.entry);
1463
+ if (!entry) {
1464
+ const manifest = join4(dir, "vitral.json");
1465
+ if (!existsSync7(manifest)) throw new Error(`[vitral] ${manifest} n\xE3o existe. Rode o build ou passe --entry.`);
1466
+ entry = resolve5(dir, JSON.parse(readFileSync7(manifest, "utf8")).entry);
1467
+ }
1468
+ const close = typeof document === "undefined" ? await installDom(cwd) : void 0;
1469
+ try {
1470
+ const mod = await import(pathToFileURL(entry).href);
1471
+ const sources3 = sourcesIn(dirname4(entry));
1472
+ const tsApi = local ? await typescript() : null;
1473
+ if (!local) log(`[vitral] sem Props local em ${localFile}: as props gravadas n\xE3o foram conferidas pelo tipo.`);
1474
+ else if (!tsApi) log("[vitral] sem TypeScript no projeto: as props gravadas n\xE3o foram conferidas pelo tipo.");
1475
+ for (const file of files2) {
1476
+ const contract = JSON.parse(readFileSync7(file, "utf8"));
1477
+ const result = await verify(contract, mod, { sources: sources3 });
1478
+ if (local && tsApi) {
1479
+ const typed = checkCaseProps(contract, local, tsApi);
1480
+ result.failures.push(...typed.failures);
1481
+ result.warnings.push(...typed.warnings);
1482
+ }
1483
+ const who = `${contract.remote}${contract.host ? ` (host ${contract.host})` : ""}`;
1484
+ for (const w of result.warnings) log(`! ${who}: ${w}`);
1485
+ for (const f of result.failures) log(`\u2717 ${who}: ${f}`);
1486
+ log(result.failures.length ? `[vitral] ${file}: ${result.failures.length} falha(s) em ${result.cases} caso(s).` : `[vitral] ${file}: ${result.cases} caso(s) passaram.`);
1487
+ failures += result.failures.length;
1488
+ }
1489
+ } finally {
1490
+ await close?.();
1491
+ }
1492
+ }
1493
+ return failures;
1494
+ }
1495
+
1153
1496
  // src/cli.ts
1154
1497
  var AJUDA = `Uso:
1155
1498
  vitral types [remotes.json | nome=url ...] [--base url] [--out arquivo] [--check]
@@ -1160,6 +1503,9 @@ var AJUDA = `Uso:
1160
1503
  [--contract faixa] [--max-kb n] [--json]
1161
1504
  vitral migrate [pasta] [--from single-spa|module-federation] [--dry-run]
1162
1505
  vitral canal <canal.json> [--stable x.y.z] [--canary x.y.z | off] [--percent n]
1506
+ vitral test [--contract arquivo|pasta ...] [--since url|arquivo] [--dir dist] [--entry arquivo]
1507
+ [--types vitral.d.ts]
1508
+ vitral dict <dist> --from <dist-anterior> [--from ...] [--level n] [--no-dcb]
1163
1509
 
1164
1510
  types Baixa o vitral.d.ts de cada remote e gera vitral-env.d.ts.
1165
1511
  --check s\xF3 confere se o hash de cada manifesto ainda \xE9 o
@@ -1177,7 +1523,16 @@ var AJUDA = `Uso:
1177
1523
  --dry-run s\xF3 mostra o que mudaria, sem escrever.
1178
1524
  canal Cria ou edita o canal de um remote (canary e rollback) e mostra
1179
1525
  como ficou. --stable \xE9 obrigat\xF3ria ao criar; --canary off tira a
1180
- canary; --percent vai de 0 a 100.`;
1526
+ canary; --percent vai de 0 a 100.
1527
+ test No CI do remote. --contract monta o build em happy-dom com cada
1528
+ caso gravado pelo host (record() de vitral/testing) e falha se o
1529
+ mount quebrar, se um evento que o host escuta sumir ou se as props
1530
+ gravadas n\xE3o baterem com o Props local. --since compara o Props
1531
+ do vitral.d.ts publicado com o local e falha nas mudan\xE7as que
1532
+ quebram o host.
1533
+ dict Pr\xE9-comprime cada arquivo do dist com a vers\xE3o anterior dele
1534
+ como dicion\xE1rio (.dcz e, com o bin\xE1rio brotli, .dcb) e grava
1535
+ vitral-dict.json para o middleware vitral/dict.`;
1181
1536
  var falhou = (err) => {
1182
1537
  console.error(err.message);
1183
1538
  process.exitCode = 1;
@@ -1189,7 +1544,7 @@ function bool(args2, name) {
1189
1544
  }
1190
1545
  function version() {
1191
1546
  try {
1192
- return createRequire(import.meta.url)("../package.json").version;
1547
+ return createRequire2(import.meta.url)("../package.json").version;
1193
1548
  } catch {
1194
1549
  return "0.0.0";
1195
1550
  }
@@ -1247,6 +1602,21 @@ if (command === "types") {
1247
1602
  } catch (err) {
1248
1603
  falhou(err);
1249
1604
  }
1605
+ } else if (command === "test") {
1606
+ const contracts = [];
1607
+ for (let c = flag(args, "--contract"); c !== void 0; c = flag(args, "--contract")) contracts.push(c);
1608
+ finish(test({ contracts, since: flag(args, "--since"), dir: flag(args, "--dir"), entry: flag(args, "--entry"), types: flag(args, "--types") }));
1609
+ } else if (command === "dict" && args.length) {
1610
+ const from = [];
1611
+ for (let f = flag(args, "--from"); f !== void 0; f = flag(args, "--from")) from.push(f);
1612
+ const level = flag(args, "--level");
1613
+ const noDcb = bool(args, "--no-dcb");
1614
+ try {
1615
+ if (!from.length) throw new Error("[vitral] dict precisa de --from <dist-anterior>");
1616
+ dict({ dist: args[0], from, level: level === void 0 ? void 0 : Number(level), brotli: noDcb ? false : void 0 });
1617
+ } catch (err) {
1618
+ falhou(err);
1619
+ }
1250
1620
  } else {
1251
1621
  console.log(AJUDA);
1252
1622
  process.exitCode = command && command !== "--help" ? 1 : 0;