vitral 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +324 -2
- package/dist/angular.d.ts +27 -0
- package/dist/angular.js +70 -0
- package/dist/chunk-IZK6ITIJ.js +96 -0
- package/dist/chunk-TTL52BF2.js +547 -0
- package/dist/element.d.ts +30 -0
- package/dist/element.js +11 -0
- package/dist/index.d.ts +191 -0
- package/dist/index.js +36 -0
- package/dist/preact.d.ts +22 -0
- package/dist/preact.js +43 -0
- package/dist/react.d.ts +32 -0
- package/dist/react.js +117 -0
- package/dist/solid.d.ts +24 -0
- package/dist/solid.js +60 -0
- package/dist/svelte.d.ts +27 -0
- package/dist/svelte.js +36 -0
- package/dist/testing.d.ts +28 -0
- package/dist/testing.js +47 -0
- package/dist/vite.d.ts +24 -0
- package/dist/vite.js +96 -0
- package/dist/vue.d.ts +61 -0
- package/dist/vue.js +125 -0
- package/package.json +195 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 João (joaogabriel15)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,325 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><img src="docs/logo.svg" width="96" alt="vitral"></p>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<h1 align="center">vitral</h1>
|
|
4
|
+
|
|
5
|
+
<p align="center"><strong>Feito de partes. Visto como um todo.</strong></p>
|
|
6
|
+
|
|
7
|
+
[](https://github.com/joaogabriel15/vitral/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/vitral)
|
|
9
|
+
[](https://bundlejs.com/?q=vitral)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install vitral
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Junte micro-frontends de qualquer framework numa página só, sem configuração de build no host.
|
|
17
|
+
|
|
18
|
+
- Cada app publica um módulo e o host só precisa da URL dele.
|
|
19
|
+
- React, Vue, Angular, Svelte e os outros convivem na mesma tela.
|
|
20
|
+
- CSS, erros e deploys ficam isolados por app.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
host (qualquer framework) remote (qualquer framework)
|
|
24
|
+
┌────────────────────────────┐ URL ┌──────────────────────────────┐
|
|
25
|
+
│ configure({ remotes }) │ ─────────▶ │ vitral.json { entry, css, │
|
|
26
|
+
│ <Remote name="carrinho" /> │ │ integrity } │
|
|
27
|
+
│ │ ◀───────── │ remote.js mount/update/ │
|
|
28
|
+
│ events (barramento global) │ import() │ unmount │
|
|
29
|
+
└────────────────────────────┘ └──────────────────────────────┘
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Por que é diferente
|
|
33
|
+
|
|
34
|
+
| Dor comum (Module Federation, single-spa) | Como o vitral resolve |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Host e remote precisam de config de build combinada | Host não tem config de build nenhuma. Só `configure({ remotes })` em runtime. |
|
|
37
|
+
| `shared` / singletons quebram com versões diferentes de React/Vue | Por padrão cada remote leva o próprio framework, isolado na sua própria root. Nenhum conflito de versão. Compartilhar é opcional (import map). |
|
|
38
|
+
| Erro de um remote derruba a página | Cada remote tem estado de loading, erro e "tentar de novo" próprios. |
|
|
39
|
+
| CSS de um vaza no outro; Tailwind do remote sem classes ou quebrando o host | O remote leva o próprio CSS, já com escopo automático. `isolate="shadow"` isola de vez, e o `@property` do Tailwind v4 continua funcionando. |
|
|
40
|
+
| Cache velho depois do deploy (`ChunkLoadError`) | Arquivos com hash; só o `vitral.json` é revalidado. |
|
|
41
|
+
| Roteadores brigando | `navigate(url)` mantém host e remotes na mesma URL. |
|
|
42
|
+
| Executar JS de outra origem sem garantia | Allowlist de origens + verificação de hash SRI antes de executar. |
|
|
43
|
+
| Muitos conceitos (containers, exposes, lifecycles, root config) | Um contrato: `mount(el, props, ctx) → { update, unmount }`. |
|
|
44
|
+
|
|
45
|
+
## Frameworks suportados
|
|
46
|
+
|
|
47
|
+
| Framework | Remote (publicar) | Host (consumir) | Import |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| React 18/19 | `reactRemote(App)` | `<Remote>` | `vitral/react` |
|
|
50
|
+
| Next.js (App e Pages Router) | igual ao React | `<Remote>` (já é `'use client'`, SSR seguro) | `vitral/react` |
|
|
51
|
+
| Vue 3 | `vueRemote(App, { setup })` | `<VitralRemote>` | `vitral/vue` |
|
|
52
|
+
| Nuxt 3/4 | igual ao Vue | `<VitralRemote>` (SSR seguro) | `vitral/vue` |
|
|
53
|
+
| Angular 17+ (standalone, com ou sem zone.js) | `angularRemote(AppComponent, { providers })` | `<vitral-remote>` + `provideVitral()` | `vitral/angular` |
|
|
54
|
+
| Svelte 5 / SvelteKit | `svelteRemote(App)` | `use:remote` ou `<vitral-remote>` | `vitral/svelte` |
|
|
55
|
+
| Solid / SolidStart | `solidRemote(App)` | `<Remote>` | `vitral/solid` |
|
|
56
|
+
| Preact | `preactRemote(App)` | `<Remote>` | `vitral/preact` |
|
|
57
|
+
| Lit, Stencil, Web Components | `elementRemote('meu-widget')` | `<vitral-remote>` | `vitral/element` |
|
|
58
|
+
| JS / TypeScript puro | `component(render)` | `<vitral-remote>` | `vitral`, `vitral/element` |
|
|
59
|
+
| Qualquer outro (Astro, Qwik, jQuery, HTML do servidor…) | `defineRemote({ mount })` | `<vitral-remote>` | `vitral/element` |
|
|
60
|
+
|
|
61
|
+
Todas as 72 combinações host × remote dos exemplos (9 hosts × 8 remotes) são
|
|
62
|
+
testadas no navegador a cada execução do e2e.
|
|
63
|
+
|
|
64
|
+
## Uso
|
|
65
|
+
|
|
66
|
+
### Publicar um remote
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// React // Vue
|
|
70
|
+
import { reactRemote } from 'vitral/react' import { vueRemote } from 'vitral/vue'
|
|
71
|
+
export default reactRemote(Carrinho) export default vueRemote(Catalogo, { setup: (app) => app.use(pinia) })
|
|
72
|
+
|
|
73
|
+
// Svelte (o contexto chega na prop `vitral`)
|
|
74
|
+
import { svelteRemote } from 'vitral/svelte'
|
|
75
|
+
export default svelteRemote(Cartao)
|
|
76
|
+
|
|
77
|
+
// Solid / Preact (o contexto chega na prop `vitral`)
|
|
78
|
+
export default solidRemote(Cartao) export default preactRemote(Cartao)
|
|
79
|
+
|
|
80
|
+
// Angular: props viram inputs; contexto com injectVitral()
|
|
81
|
+
import { angularRemote } from 'vitral/angular'
|
|
82
|
+
export default angularRemote(CartaoComponent, { providers: [provideHttpClient()] })
|
|
83
|
+
|
|
84
|
+
// Lit / Web Component: props viram propriedades; contexto em `this.vitral`
|
|
85
|
+
import { elementRemote } from 'vitral/element'
|
|
86
|
+
export default elementRemote('lit-cartao', () => customElements.define('lit-cartao', LitCartao))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
JS ou TypeScript puro, sem framework nenhum:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { component, html } from 'vitral'
|
|
93
|
+
|
|
94
|
+
export default component<{ mensagem?: string }>({
|
|
95
|
+
state: { cliques: 0 },
|
|
96
|
+
render: ({ mensagem }, state, ctx) => html`
|
|
97
|
+
<p>${mensagem}</p>
|
|
98
|
+
<button onclick=${() => state.cliques++}>cliques: ${state.cliques}</button>
|
|
99
|
+
`,
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Sem estado, basta a função: ``component(({ nome }) => html`<p>Olá, ${nome}</p>`)``.
|
|
104
|
+
Tudo que entra no `${}` é escapado, então dá para colocar dado do usuário sem
|
|
105
|
+
risco de XSS. Mudar o `state` ou receber props novas atualiza a tela sozinho.
|
|
106
|
+
Se precisar de controle total, `defineRemote({ mount(el, props, ctx) { ... } })`
|
|
107
|
+
continua disponível.
|
|
108
|
+
|
|
109
|
+
Build com o plugin (o mesmo para todos os frameworks):
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
// vite.config.js
|
|
113
|
+
import vitral from 'vitral/vite'
|
|
114
|
+
export default { plugins: [react() /* ou vue(), svelte(), solid(), angular()… */, vitral({ entry: 'src/remote.jsx' })] }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`vite build` gera `dist/remote-[hash].js`, `dist/remote-[hash].css` e
|
|
118
|
+
`dist/vitral.json` (com o hash `sha384` do JS). Suba a pasta em qualquer CDN com
|
|
119
|
+
CORS. Para Angular use `@analogjs/vite-plugin-angular` (compila em AOT).
|
|
120
|
+
|
|
121
|
+
### CSS e Tailwind
|
|
122
|
+
|
|
123
|
+
Use Tailwind (ou qualquer CSS) no remote normalmente. O plugin prefixa o CSS
|
|
124
|
+
com `[data-vitral-scope="nome"]` e transforma `:root`/`html`/`body` na raiz do
|
|
125
|
+
remote, então o preflight e o tema não vazam para o host, e o host não precisa
|
|
126
|
+
escanear os arquivos do remote. Com `isolate="shadow"`, as regras `@property`
|
|
127
|
+
do Tailwind v4 sobem para o documento para que sombras, rings e gradientes
|
|
128
|
+
funcionem. Veja [docs/PROBLEMAS.md](docs/PROBLEMAS.md).
|
|
129
|
+
|
|
130
|
+
Sem Vite? Qualquer bundler serve (webpack, Rspack, esbuild, Rollup, Angular
|
|
131
|
+
CLI): basta gerar um módulo ES cujo `default` tenha `mount(el, props, ctx)`.
|
|
132
|
+
|
|
133
|
+
### Consumir no host (React / Next)
|
|
134
|
+
|
|
135
|
+
```jsx
|
|
136
|
+
import { configure } from 'vitral'
|
|
137
|
+
import { Remote, useEventValue } from 'vitral/react'
|
|
138
|
+
|
|
139
|
+
configure({ remotes: '/remotes.json' }) // ou um objeto { nome: url }
|
|
140
|
+
|
|
141
|
+
function App() {
|
|
142
|
+
const total = useEventValue('carrinho:total')
|
|
143
|
+
return (
|
|
144
|
+
<Remote
|
|
145
|
+
name="carrinho"
|
|
146
|
+
props={{ userId: 42 }}
|
|
147
|
+
isolate="shadow"
|
|
148
|
+
fallback={<Spinner />}
|
|
149
|
+
error={(err, retry) => <button onClick={retry}>Tentar de novo</button>}
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`<Remote>` já é `'use client'`. No Next ele renderiza o `fallback` no servidor
|
|
156
|
+
e monta o remote no cliente, então funciona no App Router e no Pages Router
|
|
157
|
+
sem `dynamic(..., { ssr: false })`.
|
|
158
|
+
|
|
159
|
+
### Consumir no host (Vue / Nuxt)
|
|
160
|
+
|
|
161
|
+
```vue
|
|
162
|
+
<script setup>
|
|
163
|
+
import { VitralRemote } from 'vitral/vue'
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<template>
|
|
167
|
+
<VitralRemote name="carrinho" :props="{ userId: 42 }" isolate="shadow">
|
|
168
|
+
<template #fallback>Carregando…</template>
|
|
169
|
+
<template #error="{ error, retry }"><button @click="retry">Tentar de novo</button></template>
|
|
170
|
+
</VitralRemote>
|
|
171
|
+
</template>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
No Nuxt, chame `configure()` num plugin `.client.ts`. O componente renderiza o
|
|
175
|
+
fallback no SSR e monta o remote no cliente.
|
|
176
|
+
|
|
177
|
+
### Consumir no host (JS/TS puro, Angular, Svelte, qualquer coisa)
|
|
178
|
+
|
|
179
|
+
O Web Component `<vitral-remote>` funciona em qualquer lugar que renderize HTML:
|
|
180
|
+
|
|
181
|
+
```html
|
|
182
|
+
<script type="module">
|
|
183
|
+
import { configure } from 'vitral'
|
|
184
|
+
import { defineVitralElement } from 'vitral/element'
|
|
185
|
+
configure({ remotes: '/remotes.json' })
|
|
186
|
+
defineVitralElement()
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<vitral-remote name="carrinho" props='{"userId": 42}' isolate="shadow">
|
|
190
|
+
<p slot="fallback">Carregando…</p>
|
|
191
|
+
<p slot="error">Indisponível</p>
|
|
192
|
+
</vitral-remote>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`el.props = {...}` atualiza sem remontar, `el.retry()` remonta, e os eventos
|
|
196
|
+
`vitral-ready` / `vitral-error` avisam o estado.
|
|
197
|
+
|
|
198
|
+
**Angular:**
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
bootstrapApplication(App, { providers: [provideVitral({ remotes: '/remotes.json' })] })
|
|
202
|
+
|
|
203
|
+
@Component({
|
|
204
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
205
|
+
template: `<vitral-remote name="carrinho" [props]="{ userId: id() }" (vitral-error)="falhou()"></vitral-remote>`,
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Svelte:** `<div use:remote={{ name: 'carrinho', props: { userId } }}></div>`
|
|
210
|
+
(com `import { remote } from 'vitral/svelte'`).
|
|
211
|
+
|
|
212
|
+
**Solid / Preact:** `<Remote name="carrinho" props={{ userId }} fallback={…} error={(e, retry) => …} />`.
|
|
213
|
+
|
|
214
|
+
### Comunicação
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
import { events } from 'vitral'
|
|
218
|
+
events.emit('carrinho:adicionar', produto)
|
|
219
|
+
const off = events.on('carrinho:adicionar', (p) => …)
|
|
220
|
+
events.last('usuario') // último valor emitido
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Dentro de remotes: `useVitral()` (contexto), `useEvent(tipo, fn)` e
|
|
224
|
+
`useEventValue(tipo)` existem em `vitral/react` e `vitral/vue`. O barramento é o
|
|
225
|
+
mesmo para todos, mesmo que cada remote tenha sua cópia da lib, e o que o
|
|
226
|
+
remote assina por `ctx.events` é cancelado sozinho no unmount.
|
|
227
|
+
|
|
228
|
+
Para navegar de dentro de um remote, use `navigate('/pedidos/42')` (ou
|
|
229
|
+
`ctx.navigate`). Ele atualiza o histórico e avisa todos os roteadores.
|
|
230
|
+
|
|
231
|
+
### Erros
|
|
232
|
+
|
|
233
|
+
Todo erro é um `RemoteError` com `code` e o nome do remote. Falhas de
|
|
234
|
+
download e erros de render dentro do remote (React, Vue) aparecem no `error`
|
|
235
|
+
do componente de host (ou no slot `error` do `<vitral-remote>`), com `retry`
|
|
236
|
+
para remontar só aquele remote.
|
|
237
|
+
|
|
238
|
+
### Configuração
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
configure({
|
|
242
|
+
remotes: { carrinho: 'https://cdn.empresa.com/carrinho/vitral.json' }, // ou URL de um JSON
|
|
243
|
+
allowedOrigins: ['https://cdn.empresa.com'], // padrão: página + origens registradas
|
|
244
|
+
timeout: 15000,
|
|
245
|
+
retries: 1,
|
|
246
|
+
isolate: 'none', // ou 'shadow' para todos
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Entradas podem ser objetos: `{ url, integrity, css, isolate }`, ou
|
|
251
|
+
`{ module: () => import('./carrinho') }` para um remote local (monorepo) sem
|
|
252
|
+
rede. A `integrity`
|
|
253
|
+
declarada pelo host vence a do manifesto (fixe o hash no host quando o remote
|
|
254
|
+
for de terceiros).
|
|
255
|
+
|
|
256
|
+
## Segurança
|
|
257
|
+
|
|
258
|
+
- **Allowlist de origens**: nenhum código é importado de origem não permitida,
|
|
259
|
+
nem mesmo se um manifesto apontar para fora.
|
|
260
|
+
- **SRI**: quando há `integrity`, a lib baixa o JS, confere o hash e executa
|
|
261
|
+
exatamente os bytes verificados (via `blob:`). Com CSP, inclua `blob:` em
|
|
262
|
+
`script-src`.
|
|
263
|
+
- **Sem `eval`**, sem iframes, sem globais além de um `Symbol.for('vitral.state.v1')`.
|
|
264
|
+
- Shadow DOM isola CSS, não JS: um remote ainda roda com os privilégios da
|
|
265
|
+
página. Para código não confiável de verdade, use iframe.
|
|
266
|
+
|
|
267
|
+
## Dependências compartilhadas (opcional)
|
|
268
|
+
|
|
269
|
+
Para não baixar React/Vue duas vezes, marque-os como `shared` no remote e
|
|
270
|
+
declare um import map no host:
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
vitral({ entry: 'src/remote.jsx', shared: ['react', 'react-dom', 'react-dom/client'] })
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```html
|
|
277
|
+
<script type="importmap">
|
|
278
|
+
{ "imports": { "react": "https://esm.sh/react@19.2.0", "react-dom/client": "https://esm.sh/react-dom@19.2.0/client" } }
|
|
279
|
+
</script>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Testes
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
// Teste do host sem subir nenhum remote
|
|
286
|
+
import { mockRemote, failingRemote } from 'vitral/testing'
|
|
287
|
+
|
|
288
|
+
const carrinho = mockRemote('carrinho', (el, props) => (el.textContent = `itens: ${props.itens}`))
|
|
289
|
+
// …renderize o host…
|
|
290
|
+
expect(carrinho.lastProps).toEqual({ itens: 2 })
|
|
291
|
+
failingRemote('recomendacoes') // para testar a tela de erro
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
No repositório (veja [CONTRIBUTING.md](CONTRIBUTING.md)):
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
pnpm install
|
|
298
|
+
pnpm check # build, tipos, publint/attw, tamanho e testes unitários
|
|
299
|
+
pnpm test:e2e # builda tudo e roda o Playwright: 9 hosts × 8 remotes + demo
|
|
300
|
+
pnpm examples # sobe os exemplos (hosts em :4201-:4207, remotes em :4100)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
O CI roda tudo isso em Node 20, 22 e 24 a cada push e PR. Versões novas saem
|
|
304
|
+
pelo Changesets: `pnpm changeset`, merge do PR "Versionar pacote" e o GitHub
|
|
305
|
+
Actions publica no npm com proveniência.
|
|
306
|
+
|
|
307
|
+
## Exemplos e demo
|
|
308
|
+
|
|
309
|
+
- `examples/remotes/*`: o mesmo cartão em React, Vue, Svelte, Solid, Preact,
|
|
310
|
+
Angular, Lit e TypeScript puro.
|
|
311
|
+
- `examples/hosts/*`: hosts em JS puro (:4201), React (:4202), Vue (:4203),
|
|
312
|
+
Svelte (:4204), Solid (:4205), Preact (:4206), Angular (:4207), Next (:4208,
|
|
313
|
+
`pnpm --dir examples/hosts/next start`) e Nuxt (:4209,
|
|
314
|
+
`pnpm --dir examples/hosts/nuxt start`), cada um carregando os 8 remotes.
|
|
315
|
+
- `demo/`: catálogo Vue + carrinho React com Tailwind (`pnpm demo`, em
|
|
316
|
+
http://localhost:5173 e :5174).
|
|
317
|
+
|
|
318
|
+
## Estado
|
|
319
|
+
|
|
320
|
+
Protótipo 0.1. Testado (32 testes unitários e 43 e2e): carregamento via
|
|
321
|
+
manifesto com SRI, Shadow DOM, props reativas sem remontar, props que mudam
|
|
322
|
+
durante o download, desmontar/remontar, StrictMode do React, erros de download
|
|
323
|
+
e de render, eventos entre frameworks, Tailwind v4 com e sem Shadow DOM, escopo
|
|
324
|
+
de CSS e SSR em Next e Nuxt. O que ainda falta está em
|
|
325
|
+
[docs/PROBLEMAS.md](docs/PROBLEMAS.md#ainda-não-resolvidos-próximos-passos-sugeridos).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { InjectionToken, Type, ApplicationConfig, Provider, EnvironmentProviders } from '@angular/core';
|
|
2
|
+
import { RemoteContext, Props, RemoteModule, VitralConfig } from './index.js';
|
|
3
|
+
export { events, navigate } from './index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adaptador Angular (standalone, zoneless ou com zone.js).
|
|
7
|
+
*
|
|
8
|
+
* Remote: export default angularRemote(AppComponent, { providers: [provideHttpClient()] })
|
|
9
|
+
* Props viram inputs (`input()` ou `@Input()`); o contexto vem de `inject(VITRAL_CONTEXT)`.
|
|
10
|
+
*
|
|
11
|
+
* Host: providers: [provideVitral({ remotes: '/remotes.json' })]
|
|
12
|
+
* schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
13
|
+
* <vitral-remote name="carrinho" [props]="{ userId }" isolate="shadow"></vitral-remote>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
declare const VITRAL_CONTEXT: InjectionToken<RemoteContext>;
|
|
17
|
+
/** Transforma um componente standalone em um remote. */
|
|
18
|
+
declare function angularRemote<P extends Props>(component: Type<unknown>, config?: Partial<ApplicationConfig>): RemoteModule<P>;
|
|
19
|
+
/** Dentro de um remote Angular: `const vitral = injectVitral()`. */
|
|
20
|
+
declare function injectVitral(): RemoteContext | null;
|
|
21
|
+
/**
|
|
22
|
+
* Host Angular: configura o vitral e registra `<vitral-remote>`. Use junto com
|
|
23
|
+
* `CUSTOM_ELEMENTS_SCHEMA` no componente que renderiza o remote.
|
|
24
|
+
*/
|
|
25
|
+
declare function provideVitral(config?: VitralConfig): (Provider | EnvironmentProviders)[];
|
|
26
|
+
|
|
27
|
+
export { VITRAL_CONTEXT, angularRemote, injectVitral, provideVitral };
|
package/dist/angular.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineVitralElement
|
|
3
|
+
} from "./chunk-IZK6ITIJ.js";
|
|
4
|
+
import {
|
|
5
|
+
configure,
|
|
6
|
+
defineRemote,
|
|
7
|
+
events,
|
|
8
|
+
navigate
|
|
9
|
+
} from "./chunk-TTL52BF2.js";
|
|
10
|
+
|
|
11
|
+
// src/angular.ts
|
|
12
|
+
import {
|
|
13
|
+
InjectionToken,
|
|
14
|
+
createComponent,
|
|
15
|
+
inject,
|
|
16
|
+
provideAppInitializer,
|
|
17
|
+
provideZonelessChangeDetection,
|
|
18
|
+
reflectComponentType
|
|
19
|
+
} from "@angular/core";
|
|
20
|
+
import { createApplication } from "@angular/platform-browser";
|
|
21
|
+
var VITRAL_CONTEXT = new InjectionToken("vitral.context");
|
|
22
|
+
function angularRemote(component, config = {}) {
|
|
23
|
+
const inputs = new Set((reflectComponentType(component)?.inputs ?? []).map((i) => i.templateName));
|
|
24
|
+
return defineRemote({
|
|
25
|
+
async mount(el, props, ctx) {
|
|
26
|
+
const app = await createApplication({
|
|
27
|
+
providers: [provideZonelessChangeDetection(), { provide: VITRAL_CONTEXT, useValue: ctx }, ...config.providers ?? []]
|
|
28
|
+
});
|
|
29
|
+
const hostElement = document.createElement("div");
|
|
30
|
+
el.appendChild(hostElement);
|
|
31
|
+
const ref = createComponent(component, { environmentInjector: app.injector, hostElement });
|
|
32
|
+
const apply = (p) => {
|
|
33
|
+
for (const [k, v] of Object.entries(p)) if (inputs.has(k)) ref.setInput(k, v);
|
|
34
|
+
};
|
|
35
|
+
apply(props);
|
|
36
|
+
app.attachView(ref.hostView);
|
|
37
|
+
ref.changeDetectorRef.detectChanges();
|
|
38
|
+
return {
|
|
39
|
+
update(next) {
|
|
40
|
+
apply(next);
|
|
41
|
+
app.tick();
|
|
42
|
+
},
|
|
43
|
+
unmount() {
|
|
44
|
+
app.destroy();
|
|
45
|
+
hostElement.remove();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function injectVitral() {
|
|
52
|
+
return inject(VITRAL_CONTEXT, { optional: true });
|
|
53
|
+
}
|
|
54
|
+
function provideVitral(config = {}) {
|
|
55
|
+
return [
|
|
56
|
+
provideAppInitializer(() => {
|
|
57
|
+
if (typeof window === "undefined") return;
|
|
58
|
+
configure(config);
|
|
59
|
+
defineVitralElement();
|
|
60
|
+
})
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
VITRAL_CONTEXT,
|
|
65
|
+
angularRemote,
|
|
66
|
+
events,
|
|
67
|
+
injectVitral,
|
|
68
|
+
navigate,
|
|
69
|
+
provideVitral
|
|
70
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSlot,
|
|
3
|
+
defineRemote
|
|
4
|
+
} from "./chunk-TTL52BF2.js";
|
|
5
|
+
|
|
6
|
+
// src/element.ts
|
|
7
|
+
var Base = typeof HTMLElement === "undefined" ? class {
|
|
8
|
+
} : HTMLElement;
|
|
9
|
+
var VitralRemoteElement = class extends Base {
|
|
10
|
+
static {
|
|
11
|
+
this.observedAttributes = ["name", "props", "isolate"];
|
|
12
|
+
}
|
|
13
|
+
#slot = null;
|
|
14
|
+
#props = {};
|
|
15
|
+
get props() {
|
|
16
|
+
return this.#props;
|
|
17
|
+
}
|
|
18
|
+
set props(value) {
|
|
19
|
+
this.#props = value ?? {};
|
|
20
|
+
this.#slot?.update(this.#props);
|
|
21
|
+
}
|
|
22
|
+
get status() {
|
|
23
|
+
return this.#slot?.status ?? "loading";
|
|
24
|
+
}
|
|
25
|
+
retry() {
|
|
26
|
+
this.#slot?.retry();
|
|
27
|
+
}
|
|
28
|
+
connectedCallback() {
|
|
29
|
+
if (this.#slot) return;
|
|
30
|
+
const name = this.getAttribute("name");
|
|
31
|
+
if (!name) return;
|
|
32
|
+
if (this.hasAttribute("props") && !Object.keys(this.#props).length) this.#props = parseProps(this.getAttribute("props"));
|
|
33
|
+
this.#slot = createSlot(this, {
|
|
34
|
+
name,
|
|
35
|
+
props: this.#props,
|
|
36
|
+
isolate: this.getAttribute("isolate") || void 0,
|
|
37
|
+
onStatus: (status, error) => {
|
|
38
|
+
this.setAttribute("data-vitral-status", status);
|
|
39
|
+
for (const el of this.querySelectorAll(':scope > [slot="fallback"]')) el.hidden = status !== "loading";
|
|
40
|
+
for (const el of this.querySelectorAll(':scope > [slot="error"]')) el.hidden = status !== "error";
|
|
41
|
+
if (status === "ready") this.dispatchEvent(new CustomEvent("vitral-ready", { bubbles: true }));
|
|
42
|
+
if (status === "error") this.dispatchEvent(new CustomEvent("vitral-error", { bubbles: true, detail: { error } }));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
disconnectedCallback() {
|
|
47
|
+
queueMicrotask(() => {
|
|
48
|
+
if (!this.isConnected) {
|
|
49
|
+
this.#slot?.destroy();
|
|
50
|
+
this.#slot = null;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
attributeChangedCallback(attr, _old, value) {
|
|
55
|
+
if (attr === "props") return void (this.props = parseProps(value));
|
|
56
|
+
if (!this.#slot) return void (this.isConnected && this.connectedCallback());
|
|
57
|
+
if (attr === "name" && value) this.#slot.set({ name: value });
|
|
58
|
+
if (attr === "isolate") this.#slot.set({ isolate: value || void 0 });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function parseProps(value) {
|
|
62
|
+
if (!value) return {};
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(value);
|
|
65
|
+
} catch {
|
|
66
|
+
console.error("[vitral] atributo props n\xE3o \xE9 JSON v\xE1lido:", value);
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function defineVitralElement(tag = "vitral-remote") {
|
|
71
|
+
if (typeof customElements !== "undefined" && !customElements.get(tag)) {
|
|
72
|
+
customElements.define(tag, class extends VitralRemoteElement {
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function elementRemote(tag, setup) {
|
|
77
|
+
return defineRemote({
|
|
78
|
+
mount(el, props, ctx) {
|
|
79
|
+
setup?.();
|
|
80
|
+
const node = document.createElement(tag);
|
|
81
|
+
node.vitral = ctx;
|
|
82
|
+
Object.assign(node, props);
|
|
83
|
+
el.appendChild(node);
|
|
84
|
+
return {
|
|
85
|
+
update: (next) => Object.assign(node, next),
|
|
86
|
+
unmount: () => node.remove()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
VitralRemoteElement,
|
|
94
|
+
defineVitralElement,
|
|
95
|
+
elementRemote
|
|
96
|
+
};
|