TMF

@tmf/platform-core (6.12.0-rc.2)

Published 2026-11-04 02:00:47 -03:00 by tarcisio

Installation

@tmf:registry=
npm install @tmf/platform-core@6.12.0-rc.2
"@tmf/platform-core": "6.12.0-rc.2"

About this package

@tmf/platform-core

Base transversal do Modelo A da plataforma TMF: o pacote agnóstico de produto que todo módulo @tmf/* adota para falar com o host e com o Hub. Reúne o HttpClient (fetch nativo com Bearer + X-Client-Code), o cliente do contrato do Hub (createHubContractClient / useAppContract, com suporte a ETag), o ModuleProvider que recebe os ports injetados pelo host (i18n, toast, theme, http) e expõe contract, tenant, user, permissions e featureFlags por contexto, além das permissões (can / usePermission / Gate) e dos adapters opcionais (sonner, react-i18next, design-tokens). Não contém nenhuma lógica de domínio — é só o contrato de integração.

Versão atual: 6.12.0-rc.2 — linha gerada de package.json por scripts/gen-packages-table.py; não editar à mão.

Nota: @tmf/module-kit é o nome histórico deste pacote. Hoje todo import usa @tmf/platform-core; o nome antigo só sobrevive como palavra-chave em package.json e no checklist legado do USAGE.md.

Instalação

O pacote é privado e servido pelo registry npm do Forgejo (organização TMF):

npm config set @tmf:registry https://git.trademarketingforce.com/api/packages/TMF/npm/
npm i @tmf/platform-core

react e react-dom (>=18) são peerDependencies obrigatórias. sonner, react-i18next e @tmf/design-tokens são peerDeps opcionais — só necessárias se você importar os adapters correspondentes.

Uso básico

1. HttpClient — createHttpClient

Cliente HTTP sobre fetch nativo (sem axios). Injeta Authorization: Bearer via getToken e X-Client-Code quando clientCode é definido (multi-tenant). Em resposta não-ok lança HttpError (com status, body, url, method); há retry automático para 429/5xx — por padrão 2 tentativas extras, com backoff exponencial, só em GET/HEAD (configurável por retry; maxRetries: 0 desliga):

import { createHttpClient } from '@tmf/platform-core';

const http = createHttpClient({
  baseUrl: 'https://hub.trademarketingforce.com',
  getToken: () => getMemoryToken() ?? '',
  clientCode: 'ACME', // opcional — vira X-Client-Code
});

const tickets = await http.request<Ticket[]>('/api/tickets');

Opções para hosts reais (todas aditivas — request continua devolvendo só o data):

Opção Onde Efeito
credentials cliente e request Repassado ao fetch ('include' p/ cookie). A request sobrescreve o cliente.
body cru request FormData, Blob, ArrayBuffer/views e URLSearchParams vão sem JSON.stringify e sem Content-Type forçado (o fetch define o boundary). Demais valores seguem como JSON.
onUploadProgress request O fetch não tem progresso de upload. Quando informado (e XMLHttpRequest existe — só navegador), a request é feita via XHR, com os mesmos headers/credentials/signal/timeout; sem ele, usa fetch. Recebe { loaded, total, progress? }.
responseType request 'json' (padrão) | 'text' | 'blob' | 'arraybuffer'.
params request Objeto serializado na query: arrays repetem a chave, undefined/null são omitidos, mescla com ?x= já presente no path.
timeout (ms) cliente e request Aborta via AbortController, combinado com o signal do chamador. Estouro lança HttpTimeoutError (code: 'ETIMEDOUT'); abort do chamador segue sendo AbortError.
requestFull cliente Como request, mas devolve { data, status, headers } (headers com nomes em minúsculas).
retry cliente e request Retry de 429/5xx (padrão: GET/HEAD, 2 tentativas adicionais, backoff 200ms). false desliga (1 chamada); true = padrão; no cliente aceita também { maxRetries, baseDelayMs, methods }; na request, { attempts }. A request sobrescreve o cliente.
onUnauthorized cliente () => Promise<void | boolean>. Em 401, renova a credencial (getToken passa a devolver o token novo) uma vez para N requests simultâneas; cada uma se repete uma vez. false/rejeição = falha: rejeita com o HttpError 401 original. A repetição com 401 não dispara novo refresh.
onAuthFailure cliente (error: HttpError) => void, chamado quando o refresh falha — ponto para deslogar.

HttpError expõe error.response = { status, data, headers } (compatível com quem lê error.response do axios), além de status, body e headers.

const { data, headers } = await http.requestFull<Row[]>('/api/rows', { params: { ids: [1, 2], q: 'x' } });
const pdf = await http.request<Blob>('/api/report', { responseType: 'blob', timeout: 30_000 });
await http.request('/api/upload', { method: 'POST', body: formData, onUploadProgress: (e) => setPct(e.progress ?? 0) });

2. Contrato do Hub — createHubContractClient / useAppContract

Busca o AppContract em GET /api/platform/app-contract, normalizando o wire snake_case do Hub para o AppContract canônico (camelCase). Suporta cache condicional via ETag (If-None-Match → 304):

import { useAppContract } from '@tmf/platform-core';

const { contract, loading, error, refresh } = useAppContract({
  baseUrl: 'https://hub.trademarketingforce.com',
  getToken: () => getMemoryToken() ?? '',
});

O hook faz refresh automático por contract.ttlSeconds (ou por refreshIntervalMs, se informado; fallback de 60s). Para uso imperativo, use createHubContractClient({ baseUrl, getToken }).fetch(etag).

error não é o sinal de tela de erro — contract === null é. Um refresh em background que falha (aba inativa, token expirado, rede) mantém o contrato anterior e preenche error. O contrato continua válido. Só a 1ª carga sem sucesso deixa contract nulo, e nesse caso o hook já retenta com backoff (5s → 15s → 60s):

const { contract, loading, error } = useAppContract({ baseUrl, getToken });
if (loading) return <Spinner />;
if (!contract) return <Indisponivel />;        // ✅ fail-closed só sem contrato
if (error) console.warn('[contract] refresh falhou, usando cache:', error.message);
return <ModuleProvider contract={contract}>…</ModuleProvider>;

// ❌ if (error || !contract) — derruba o módulo quando um refresh falha (TH-64619)

3. Provider — ModuleProvider

Monta o root do módulo com o contrato e os ports injetados pelo host. Aplica theme.cssVars no :root, carrega locale.messages no i18n e cria o http automaticamente se não for passado:

import { ModuleProvider } from '@tmf/platform-core';
import { toast } from 'sonner';
import { createSonnerToastPort } from '@tmf/platform-core/adapters/sonner';

<ModuleProvider
  contract={contract}
  moduleKey="meu-modulo"   // seleciona services[key='meu-modulo'] para o http default
  toast={createSonnerToastPort(toast)}   // instância do sonner injetada pelo host
  getToken={() => getMemoryToken() ?? ''}
>
  <MeuModulo />
</ModuleProvider>

moduleKey resolve a baseUrl do http pelo service do próprio módulo em contract.services[] (key, ou alias de wire module) — sem entrada, cai em '' (same-origin) com aviso, nunca no service de outro módulo. Omitido, um contrato com um único service usa esse service; com vários, services[0] (legado). [TH-63406]

Dentro da árvore, consuma via hooks: useModule(), useTenant(), useCurrentUser(), useModuleHttp(), useFeatureFlag(key), useServiceEndpoint(key). Guarde permissões com usePermission('perm') ou <Gate require="perm">…</Gate>.

4. Escopo (ScopeContext v2), seções, boundary e widgets

  • usePrincipal() devolve { ref, can, permissions } e useScope() devolve { tenantId, clientId, principal } — o mesmo vocabulário do ScopeContext v2 do back (CONTRATO-MODULO-MONTAVEL §2). can é match exato, sem wildcard nem hierarquia; useClientId() expõe a ref opaca do cliente (vem do gateway do host, nunca do request).
  • permissions.granted deve ser sempre emitido pelo host como string[]; ausente/não-array vira false em toda verificação (fail-closed) e, em desenvolvimento, um console.error (hostGrantState classifica o estado).
  • ModuleSections / sectionAllowed / allowedSections: autorização de navegação por seção, fail-closed (CONTRATO §2.2).
  • ModuleBoundary (error boundary por módulo, dever D7 do host), ModuleHeader, ModuleSubTabs (submenu segmentado), ModuleVersionFooter + useModuleMeta (versão npm + PyPI) e PACKAGE_VERSION.
  • defineWidget / WidgetShell (também em @tmf/platform-core/widgets): contrato de widgets das libs (ADR-011); widget não recebe prop de acesso.
  • @tmf/platform-core/testing exporta makeMockContract e os ports no-op para testes.

Detalhes e exemplos: INTEGRATION.md §4.

Posição no Modelo A

@tmf/platform-core é a base de todos os módulos da plataforma. No Modelo A, a configuração (tenant, usuário, permissões, tema, locale, feature flags, endpoints de serviço) é servida pelo Hub via app-contract e entregue ao módulo por contexto — o módulo nunca lê localStorage direto nem importa sonner/react-i18next/design-tokens diretamente, apenas via ports/adapters do core.

Isso mantém o pacote agnóstico de produto: ele define o contrato de integração (ports, contrato Hub, permissões, provider) e nenhum comportamento de negócio. Os fronts @tmf/*-front do monorepo consomem o core embutido no bundle (devDependency file:../../platform-core + noExternal no tsup) — ver INTEGRATION.md §3. A conformidade estrutural do próprio pacote está em CONFORMANCE.md.

Dependencies

Development dependencies

ID Version
@dnd-kit/core ^6.3.0
@types/react ^18.2.47
@types/react-dom ^18.2.18
jsdom ^25.0.1
sonner ^1.7.4
typescript ^5.3.3

Peer dependencies

ID Version
@dnd-kit/core >=6.0.0
@tmf/design-tokens >=0.6.0
react >=18.0.0
react-dom >=18.0.0

Keywords

tmf module-kit platform contract
Details
npm
2026-11-04 02:00:47 -03:00
0
Trade Marketing Force LTDA
UNLICENSED
72 KiB
Assets (1)
Versions (28) View all
6.13.0-rc.1 2026-11-04
6.12.0-rc.2 2026-11-04
6.12.0-rc.1 2026-11-03
6.11.0 2026-10-29
6.10.0 2026-10-20