vite-electron-app 1.2.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 +449 -0
- package/dist/index.mjs +360 -0
- package/electron/electron-builder.json5 +43 -0
- package/electron/electron-env.d.ts +12 -0
- package/electron/main.ts +57 -0
- package/electron/package.json +7 -0
- package/electron/preload.ts +11 -0
- package/index.js +3 -0
- package/package.json +63 -0
- package/template-react-ts/_gitignore +24 -0
- package/template-react-ts/index.html +12 -0
- package/template-react-ts/package.json +22 -0
- package/template-react-ts/public/icon.icns +0 -0
- package/template-react-ts/public/icon.ico +0 -0
- package/template-react-ts/public/icon.png +0 -0
- package/template-react-ts/public/vite.svg +1 -0
- package/template-react-ts/src/App.tsx +34 -0
- package/template-react-ts/src/assets/react.svg +1 -0
- package/template-react-ts/src/globals.css +109 -0
- package/template-react-ts/src/main.tsx +10 -0
- package/template-react-ts/src/vite-env.d.ts +1 -0
- package/template-react-ts/tsconfig.json +25 -0
- package/template-react-ts/tsconfig.node.json +11 -0
- package/template-react-ts/vite.config.ts +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 João M
|
|
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
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/logo.svg" alt="vite-electron-app" width="200"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">vite-electron-app</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
CLI tool para criar rapidamente aplicações desktop com Electron + Vite + React + TypeScript
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/vite-electron-app">
|
|
13
|
+
<img src="https://img.shields.io/npm/v/vite-electron-app?style=flat-square" alt="npm version">
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/vite-electron-app">
|
|
16
|
+
<img src="https://img.shields.io/npm/dm/vite-electron-app?style=flat-square" alt="npm downloads">
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://github.com/joaomjbraga/vite-electron-app/blob/main/LICENSE">
|
|
19
|
+
<img src="https://img.shields.io/github/license/joaomjbraga/vite-electron-app?style=flat-square" alt="license">
|
|
20
|
+
</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📚 Índice
|
|
26
|
+
|
|
27
|
+
- [🚀 Quick Start](#-quick-start)
|
|
28
|
+
- [✨ Features](#-features)
|
|
29
|
+
- [⚡ Stack](#-stack)
|
|
30
|
+
- [🎯 Uso](#-uso)
|
|
31
|
+
- [📁 Estrutura do Projeto](#-estrutura-do-projeto)
|
|
32
|
+
- [🧠 Arquitetura](#-arquitetura)
|
|
33
|
+
- [🔐 Segurança](#-segurança)
|
|
34
|
+
- [🧪 Desenvolvimento](#-desenvolvimento)
|
|
35
|
+
- [📦 Build](#-build)
|
|
36
|
+
- [🖥️ Gerar Executáveis](#️-gerar-executáveis)
|
|
37
|
+
- [🛠️ Configuração](#️-configuração)
|
|
38
|
+
- [🤝 Contribuição](#-contribuição)
|
|
39
|
+
- [📜 Licença](#-licença)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🚀 Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Usando npx (recomendado)
|
|
47
|
+
npx vite-electron-app meu-app
|
|
48
|
+
|
|
49
|
+
# Ou instale globalmente
|
|
50
|
+
npm install -g vite-electron-app
|
|
51
|
+
vite-electron-app meu-app
|
|
52
|
+
|
|
53
|
+
# Entre no projeto
|
|
54
|
+
cd meu-app
|
|
55
|
+
|
|
56
|
+
# Instale as dependências
|
|
57
|
+
pnpm install
|
|
58
|
+
|
|
59
|
+
# Inicie o desenvolvimento
|
|
60
|
+
pnpm dev
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **Nota**: Este projeto recomenda o uso do [pnpm](https://pnpm.io/pt/) como gerenciador de pacotes.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## ✨ Features
|
|
68
|
+
|
|
69
|
+
- ⚡ **Scaffolding Rápido** - Crie projetos Electron em segundos
|
|
70
|
+
- 🔷 **TypeScript Nativo** - Código tipado do início ao fim
|
|
71
|
+
- ⚛️ **React Moderno** - Com Vite para desenvolvimento ágil
|
|
72
|
+
- 🔐 **Seguro por Padrão** - Configurações de segurança otimizadas
|
|
73
|
+
- 🛡️ **Proteção contra Arquivos Perigosos** - Bloqueia .env, chaves, certificados
|
|
74
|
+
- 🧩 **Electron Builder** - Empacotamento para Windows, macOS e Linux
|
|
75
|
+
- 📁 **Estrutura Organizada** - Separação clara de responsabilidades
|
|
76
|
+
- ♻️ **Hot Reload** - Atualização instantânea no desenvolvimento
|
|
77
|
+
- 🧪 **Testes Incluídos** - Estrutura para testes com Vitest
|
|
78
|
+
- 🔄 **Idempotente** - Scaffolding pode ser executado múltiplas vezes sem duplicações
|
|
79
|
+
- 🎨 **Mensagens Coloridas** - Output visual com cores e ícones
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ⚡ Stack
|
|
84
|
+
|
|
85
|
+
| Tecnologia | Descrição |
|
|
86
|
+
|-----------|-----------|
|
|
87
|
+
| [Electron](https://www.electronjs.org/) | Framework para criar aplicações desktop nativas |
|
|
88
|
+
| [Vite](https://vitejs.dev/) | Build tool ultrarrápido |
|
|
89
|
+
| [React](https://react.dev/) | Biblioteca para construção de interfaces |
|
|
90
|
+
| [TypeScript](https://www.typescriptlang.org/) | Superset JavaScript com tipos |
|
|
91
|
+
| [Electron Builder](https://www.electron.build/) | Empacotamento de aplicações |
|
|
92
|
+
| [Vitest](https://vitest.dev/) | Framework de testes |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 🎯 Uso
|
|
97
|
+
|
|
98
|
+
### Criar um novo projeto
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Sintaxe básica
|
|
102
|
+
npx vite-electron-app <nome-do-projeto>
|
|
103
|
+
|
|
104
|
+
# Exemplos
|
|
105
|
+
npx vite-electron-app my-electron-app
|
|
106
|
+
npx vite-electron-app desktop-app
|
|
107
|
+
npx vite-electron-app agenda-eletronica
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Opções do Projeto
|
|
111
|
+
|
|
112
|
+
Durante a criação, você será solicitado a:
|
|
113
|
+
|
|
114
|
+
1. **Nome do projeto** - Nome da pasta que será criada
|
|
115
|
+
2. **Diretório existente** - Se o diretório já existir, será perguntado se deseja sobrescrever
|
|
116
|
+
3. **Nome do pacote (package.json)** - Nome válido para o npm (opcional)
|
|
117
|
+
4. **Template** - Escolha o template inicial (React + TypeScript)
|
|
118
|
+
|
|
119
|
+
### Navegação Pós-Criação
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cd meu-app # Entre no projeto
|
|
123
|
+
pnpm install # Instale as dependências
|
|
124
|
+
pnpm dev # Inicie o desenvolvimento
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 📁 Estrutura do Projeto
|
|
130
|
+
|
|
131
|
+
Após criar um novo projeto, você terá a seguinte estrutura:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
meu-app/
|
|
135
|
+
├── electron/ # Processo Principal (Main Process)
|
|
136
|
+
│ ├── main.ts # Entry point do Electron
|
|
137
|
+
│ ├── preload.ts # Script de preload (comunicação segura)
|
|
138
|
+
│ ├── electron-env.d.ts # Tipos TypeScript
|
|
139
|
+
│ ├── electron-builder.json5 # Configuração do Electron Builder
|
|
140
|
+
│ └── public/ # Arquivos públicos do Electron
|
|
141
|
+
│ ├── icon.ico # Ícone Windows
|
|
142
|
+
│ ├── icon.png # Ícone Linux
|
|
143
|
+
│ └── icon.icns # Ícone macOS
|
|
144
|
+
│
|
|
145
|
+
├── src/ # Processo de Renderização (Renderer)
|
|
146
|
+
│ ├── App.tsx # Componente principal React
|
|
147
|
+
│ ├── App.css # Estilos do App
|
|
148
|
+
│ ├── main.tsx # Entry point React
|
|
149
|
+
│ ├── globals.css # Estilos globais
|
|
150
|
+
│ ├── index.css # Estilos base
|
|
151
|
+
│ └── assets/ # Assets do React
|
|
152
|
+
│ └── react.svg # Logo React
|
|
153
|
+
│
|
|
154
|
+
├── public/ # Arquivos públicos
|
|
155
|
+
│ └── vite.svg # Favicon
|
|
156
|
+
│
|
|
157
|
+
├── dist/ # Build de produção (renderer)
|
|
158
|
+
├── dist-electron/ # Build de produção (main/preload)
|
|
159
|
+
├── release/ # Executáveis gerados
|
|
160
|
+
│
|
|
161
|
+
├── index.html # HTML principal
|
|
162
|
+
├── package.json # Dependências e scripts
|
|
163
|
+
├── tsconfig.json # Configuração TypeScript
|
|
164
|
+
├── vite.config.ts # Configuração Vite
|
|
165
|
+
├── electron-builder.json5 # Configuração Electron Builder
|
|
166
|
+
└── .gitignore # Arquivos ignorados pelo Git
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 🧠 Arquitetura
|
|
172
|
+
|
|
173
|
+
Este projeto utiliza a arquitetura **multi-process** do Electron, separando claramente as responsabilidades:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
177
|
+
│ Aplicação Electron │
|
|
178
|
+
├─────────────────────────────────────────────────────────────┤
|
|
179
|
+
│ │
|
|
180
|
+
│ ┌──────────────────┐ ┌──────────────────────────┐ │
|
|
181
|
+
│ │ Main Process │ │ Renderer Process │ │
|
|
182
|
+
│ │ │ │ │ │
|
|
183
|
+
│ │ • Janelas │◄───────►│ • Interface React │ │
|
|
184
|
+
│ │ • Menu │ IPC │ • UI/UX │ │
|
|
185
|
+
│ │ • Tray │ │ • Lógica de apresentação│ │
|
|
186
|
+
│ │ • Sistema │ │ │ │
|
|
187
|
+
│ └──────────────────┘ └──────────────────────────┘ │
|
|
188
|
+
│ │
|
|
189
|
+
│ ┌──────────────────┐ │
|
|
190
|
+
│ │ Preload Script │ Ponte de comunicação segura │
|
|
191
|
+
│ │ (contextBridge) │ Expõe APIs específicas ao renderer │
|
|
192
|
+
│ └──────────────────┘ │
|
|
193
|
+
│ │
|
|
194
|
+
└─────────────────────────────────────────────────────────────┘
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Main Process (electron/main.ts)
|
|
198
|
+
|
|
199
|
+
O processo principal controla:
|
|
200
|
+
- Ciclo de vida da aplicação
|
|
201
|
+
- Criação e gerenciamento de janelas
|
|
202
|
+
- Acesso ao sistema de arquivos
|
|
203
|
+
- Integração com sistema operacional
|
|
204
|
+
- Menus e diálogos nativos
|
|
205
|
+
|
|
206
|
+
### Renderer Process (src/)
|
|
207
|
+
|
|
208
|
+
O processo de renderização é onde vive sua interface:
|
|
209
|
+
- Componentes React
|
|
210
|
+
- Estilos CSS
|
|
211
|
+
- Lógica de UI/UX
|
|
212
|
+
- Comunicação via IPC
|
|
213
|
+
|
|
214
|
+
### Preload Script (electron/preload.ts)
|
|
215
|
+
|
|
216
|
+
O preload é a ponte segura entre os dois processos:
|
|
217
|
+
- Expõe apenas APIs necessárias
|
|
218
|
+
- Mantém o contexto isolado
|
|
219
|
+
- Permite comunicação controlada
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🔐 Segurança
|
|
224
|
+
|
|
225
|
+
O template implementa as melhores práticas de segurança do Electron:
|
|
226
|
+
|
|
227
|
+
### Configurações Aplicadas
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// electron/main.ts
|
|
231
|
+
webPreferences: {
|
|
232
|
+
preload: path.join(__dirname, 'preload.mjs'),
|
|
233
|
+
contextIsolation: true, // ✅ Isola o contexto
|
|
234
|
+
nodeIntegration: false, // ✅ Sem acesso direto ao Node
|
|
235
|
+
sandbox: true, // ✅ Sandbox ativado
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### O que cada configuração faz:
|
|
240
|
+
|
|
241
|
+
| Configuração | Descrição | Por que é importante |
|
|
242
|
+
|-------------|-----------|---------------------|
|
|
243
|
+
| `contextIsolation: true` | Isola o contexto do renderer | Previne acesso não autorizado ao contexto principal |
|
|
244
|
+
| `nodeIntegration: false` | Desabilita require() no renderer | Impede que código malicioso acesse APIs do Node |
|
|
245
|
+
| `sandbox: true` | Ativa sandbox do Chromium | Adiciona camada extra de proteção |
|
|
246
|
+
|
|
247
|
+
### IPC Seguro
|
|
248
|
+
|
|
249
|
+
A comunicação entre processos é feita via `contextBridge`:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// electron/preload.ts
|
|
253
|
+
contextBridge.exposeInMainWorld('ipcRenderer', {
|
|
254
|
+
on: (channel, listener) => ipcRenderer.on(channel, listener),
|
|
255
|
+
send: (channel, ...args) => ipcRenderer.send(channel, ...args),
|
|
256
|
+
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 🧪 Desenvolvimento
|
|
263
|
+
|
|
264
|
+
### Scripts Disponíveis
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pnpm dev # Inicia o desenvolvimento com hot reload
|
|
268
|
+
pnpm build # Gera build de produção
|
|
269
|
+
pnpm test # Executa os testes (inclui build automático)
|
|
270
|
+
pnpm preview # Visualiza o build de produção
|
|
271
|
+
pnpm watch # Watch mode para build
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Fluxo de Desenvolvimento
|
|
275
|
+
|
|
276
|
+
1. **Inicie o servidor de desenvolvimento:**
|
|
277
|
+
```bash
|
|
278
|
+
pnpm dev
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
2. **O Electron abrirá automaticamente** com hot reload ativado.
|
|
282
|
+
|
|
283
|
+
3. **Edite os arquivos** em `src/` para ver as alterações em tempo real.
|
|
284
|
+
|
|
285
|
+
4. **Edite arquivos do Electron** em `electron/` para alterar o comportamento da aplicação.
|
|
286
|
+
|
|
287
|
+
### Hot Reload
|
|
288
|
+
|
|
289
|
+
- **React (src/)**: Atualização automática ao salvar
|
|
290
|
+
- **CSS**: Atualização automática ao salvar
|
|
291
|
+
- **Main Process**: Reinicialização automática ao alterar arquivos em `electron/`
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 📦 Build
|
|
296
|
+
|
|
297
|
+
### Build do Projeto
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
pnpm build
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Este comando:
|
|
304
|
+
1. Executa TypeScript check
|
|
305
|
+
2. Faz o build do Vite (renderer)
|
|
306
|
+
3. Faz o bundle do Electron (main/preload)
|
|
307
|
+
4. Gera os arquivos em `dist/` e `dist-electron/`
|
|
308
|
+
|
|
309
|
+
### Saída do Build
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
dist/ # Build do renderer (Vite)
|
|
313
|
+
├── index.html
|
|
314
|
+
├── assets/
|
|
315
|
+
└── ...
|
|
316
|
+
|
|
317
|
+
dist-electron/ # Build do Electron
|
|
318
|
+
├── main.js
|
|
319
|
+
└── preload.mjs
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 🖥️ Gerar Executáveis
|
|
325
|
+
|
|
326
|
+
O Electron Builder empacota sua aplicação para diferentes plataformas:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
pnpm build
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Este comando gera executáveis para:
|
|
333
|
+
|
|
334
|
+
| Plataforma | Formato | Local |
|
|
335
|
+
|-----------|---------|-------|
|
|
336
|
+
| Windows | NSIS (.exe) | `release/win-unpacked/` |
|
|
337
|
+
| macOS | DMG (.dmg) | `release/` |
|
|
338
|
+
| Linux | AppImage (.AppImage) | `release/` |
|
|
339
|
+
|
|
340
|
+
### Configuração do Build
|
|
341
|
+
|
|
342
|
+
Edite `electron-builder.json5` para personalizar:
|
|
343
|
+
|
|
344
|
+
```json5
|
|
345
|
+
{
|
|
346
|
+
"appId": "com.seu-app.id",
|
|
347
|
+
"productName": "Seu App",
|
|
348
|
+
"directories": {
|
|
349
|
+
"output": "release"
|
|
350
|
+
},
|
|
351
|
+
"win": {
|
|
352
|
+
"target": ["nsis"]
|
|
353
|
+
},
|
|
354
|
+
"mac": {
|
|
355
|
+
"target": ["dmg"]
|
|
356
|
+
},
|
|
357
|
+
"linux": {
|
|
358
|
+
"target": ["AppImage"]
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 🛠️ Configuração
|
|
366
|
+
|
|
367
|
+
### Variáveis de Ambiente
|
|
368
|
+
|
|
369
|
+
| Variável | Descrição | Padrão |
|
|
370
|
+
|----------|-----------|--------|
|
|
371
|
+
| `NODE_ENV` | Ambiente (development/production) | development |
|
|
372
|
+
| `VITE_DEV_SERVER_URL` | URL do servidor Vite (dev) | - |
|
|
373
|
+
|
|
374
|
+
### TypeScript
|
|
375
|
+
|
|
376
|
+
O projeto usa TypeScript strict mode. Edite `tsconfig.json` para ajustar:
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
{
|
|
380
|
+
"compilerOptions": {
|
|
381
|
+
"strict": true,
|
|
382
|
+
"target": "ES2020",
|
|
383
|
+
"module": "ESNext"
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Vite
|
|
389
|
+
|
|
390
|
+
Edite `vite.config.ts` para personalizar o build:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import { defineConfig } from 'vite'
|
|
394
|
+
import react from '@vitejs/plugin-react'
|
|
395
|
+
import electron from 'vite-plugin-electron/simple'
|
|
396
|
+
|
|
397
|
+
export default defineConfig({
|
|
398
|
+
plugins: [
|
|
399
|
+
react(),
|
|
400
|
+
electron({
|
|
401
|
+
main: { entry: 'electron/main.ts' },
|
|
402
|
+
preload: { input: 'electron/preload.ts' },
|
|
403
|
+
}),
|
|
404
|
+
],
|
|
405
|
+
})
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## 🤝 Contribuição
|
|
411
|
+
|
|
412
|
+
Contribuições são bem-vindas! Para contribuir:
|
|
413
|
+
|
|
414
|
+
1. **Fork o repositório**
|
|
415
|
+
2. **Crie uma branch** para sua feature: `git checkout -b feature/nova-feature`
|
|
416
|
+
3. **Commit suas mudanças**: `git commit -m 'feat: adiciona nova feature'`
|
|
417
|
+
4. **Push para a branch**: `git push origin feature/nova-feature`
|
|
418
|
+
5. **Abra um Pull Request**
|
|
419
|
+
|
|
420
|
+
### Development Setup
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
# Clone o repositório
|
|
424
|
+
git clone https://github.com/joaomjbraga/vite-electron-app.git
|
|
425
|
+
cd electron-vite-starter
|
|
426
|
+
|
|
427
|
+
# Instale dependências
|
|
428
|
+
pnpm install
|
|
429
|
+
|
|
430
|
+
# Execute os testes
|
|
431
|
+
pnpm test
|
|
432
|
+
|
|
433
|
+
# Faça o build
|
|
434
|
+
pnpm build
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## 📜 Licença
|
|
440
|
+
|
|
441
|
+
Este projeto está sob a licença [MIT](https://opensource.org/licenses/MIT).
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## 👤 Autor
|
|
446
|
+
|
|
447
|
+
**João Braga**
|
|
448
|
+
|
|
449
|
+
- GitHub: [@joaomjbraga](https://github.com/joaomjbraga)
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import prompts from "prompts";
|
|
5
|
+
//#region src/utils.ts
|
|
6
|
+
function isEmpty(dir) {
|
|
7
|
+
if (!dir || typeof dir !== "string") return true;
|
|
8
|
+
try {
|
|
9
|
+
const files = fs.readdirSync(dir);
|
|
10
|
+
return files.length === 0 || files.length === 1 && files[0] === ".git";
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function isValidPackageName(projectName) {
|
|
17
|
+
if (!projectName || projectName.length > 214) return false;
|
|
18
|
+
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName);
|
|
19
|
+
}
|
|
20
|
+
function toValidPackageName(projectName) {
|
|
21
|
+
return projectName.trim().toLowerCase().replace(/\s+/g, "-").replace(/^[._]/, "").replace(/[^a-z\d\-~]+/g, "-");
|
|
22
|
+
}
|
|
23
|
+
function emptyDir(dir) {
|
|
24
|
+
if (!fs.existsSync(dir)) return;
|
|
25
|
+
try {
|
|
26
|
+
for (const file of fs.readdirSync(dir)) {
|
|
27
|
+
if (file === ".git") continue;
|
|
28
|
+
fs.rmSync(path.resolve(dir, file), {
|
|
29
|
+
recursive: true,
|
|
30
|
+
force: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`Failed to empty directory "${dir}": ${error}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var DANGEROUS_PATTERNS = [
|
|
38
|
+
/^\.env$/i,
|
|
39
|
+
/^\.env\./i,
|
|
40
|
+
/\.key$/i,
|
|
41
|
+
/\.pem$/i,
|
|
42
|
+
/\.cert$/i,
|
|
43
|
+
/id_rsa/i,
|
|
44
|
+
/id_ed25519/i,
|
|
45
|
+
/\.npmrc$/i,
|
|
46
|
+
/\.git-credentials$/i,
|
|
47
|
+
/secrets?/i,
|
|
48
|
+
/credentials?/i
|
|
49
|
+
];
|
|
50
|
+
function isDangerousFile(filename) {
|
|
51
|
+
return DANGEROUS_PATTERNS.some((pattern) => pattern.test(filename));
|
|
52
|
+
}
|
|
53
|
+
function copy(src, dest) {
|
|
54
|
+
const srcFilename = path.basename(src);
|
|
55
|
+
if (isDangerousFile(srcFilename)) throw new Error(`Refusing to copy dangerous file: ${srcFilename}`);
|
|
56
|
+
if (!src || !dest) throw new Error("Source and destination paths are required");
|
|
57
|
+
const srcPath = path.resolve(src);
|
|
58
|
+
const destPath = path.resolve(dest);
|
|
59
|
+
if (srcPath === destPath) throw new Error("Source and destination cannot be the same");
|
|
60
|
+
try {
|
|
61
|
+
if (fs.statSync(srcPath).isDirectory()) copyDir(srcPath, destPath);
|
|
62
|
+
else fs.copyFileSync(srcPath, destPath);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Failed to copy "${src}" to "${dest}": ${error}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function copyDir(srcDir, destDir) {
|
|
68
|
+
if (!srcDir || !destDir) throw new Error("Source and destination directories are required");
|
|
69
|
+
const srcPath = path.resolve(srcDir);
|
|
70
|
+
const destPath = path.resolve(destDir);
|
|
71
|
+
try {
|
|
72
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
73
|
+
for (const file of fs.readdirSync(srcPath)) {
|
|
74
|
+
if (isDangerousFile(file)) throw new Error(`Refusing to copy dangerous file: ${file}`);
|
|
75
|
+
copy(path.join(srcPath, file), path.join(destPath, file));
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new Error(`Failed to copy directory "${srcDir}" to "${destDir}": ${error}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function editFile(file, callback) {
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
84
|
+
fs.writeFileSync(file, callback(content), "utf-8");
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Failed to edit file "${file}": ${error}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/index.ts
|
|
91
|
+
var TEMPLATE_NAME = "template-react-ts";
|
|
92
|
+
var COLOURS = {
|
|
93
|
+
$: (c) => (str) => `\x1b[${c}m` + str + "\x1B[0m",
|
|
94
|
+
reset: (str) => `\x1b[0m` + str,
|
|
95
|
+
red: (str) => COLOURS.$(31)(str),
|
|
96
|
+
green: (str) => COLOURS.$(32)(str),
|
|
97
|
+
yellow: (str) => COLOURS.$(33)(str),
|
|
98
|
+
cyan: (str) => COLOURS.$(36)(str)
|
|
99
|
+
};
|
|
100
|
+
var log = {
|
|
101
|
+
info: (msg) => console.log(msg),
|
|
102
|
+
success: (msg) => console.log(COLOURS.green("✓ ") + msg),
|
|
103
|
+
error: (msg) => console.log(COLOURS.red("✖ ") + msg),
|
|
104
|
+
warn: (msg) => console.log(COLOURS.yellow("⚠ ") + msg),
|
|
105
|
+
step: (msg) => console.log(COLOURS.cyan("→ ") + msg)
|
|
106
|
+
};
|
|
107
|
+
var cwd = process.cwd();
|
|
108
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
var argTargetDir = process.argv.slice(2).join(" ");
|
|
110
|
+
var defaultTargetDir = "electron-vite-project";
|
|
111
|
+
var renameFiles = { _gitignore: ".gitignore" };
|
|
112
|
+
async function init() {
|
|
113
|
+
let template;
|
|
114
|
+
let targetDir = (argTargetDir ?? defaultTargetDir).trim();
|
|
115
|
+
if (!targetDir) targetDir = defaultTargetDir;
|
|
116
|
+
const normalizedTargetDir = path.normalize(targetDir);
|
|
117
|
+
const resolvedTargetDir = path.resolve(cwd, normalizedTargetDir);
|
|
118
|
+
const resolvedCwd = path.resolve(cwd);
|
|
119
|
+
if (!resolvedTargetDir.startsWith(resolvedCwd) || path.isAbsolute(normalizedTargetDir)) throw new Error("Invalid target directory: path traversal not allowed");
|
|
120
|
+
const getProjectName = () => targetDir === "." ? path.basename(path.resolve()) : targetDir;
|
|
121
|
+
try {
|
|
122
|
+
template = await prompts([
|
|
123
|
+
{
|
|
124
|
+
type: () => argTargetDir ? null : "text",
|
|
125
|
+
name: "projectName",
|
|
126
|
+
message: "Project name:",
|
|
127
|
+
initial: defaultTargetDir,
|
|
128
|
+
onState: (state) => {
|
|
129
|
+
targetDir = state?.value.trim().replace(/\/+$/g, "") ?? defaultTargetDir;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: () => !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : "confirm",
|
|
134
|
+
name: "overwrite",
|
|
135
|
+
message: () => (targetDir === "." ? "Current directory" : `Target directory "${targetDir}"`) + ` is not empty. Remove existing files and continue?`
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: (_, { overwrite }) => {
|
|
139
|
+
if (overwrite === false) throw new Error(COLOURS.red("✖") + " Operation cancelled");
|
|
140
|
+
return null;
|
|
141
|
+
},
|
|
142
|
+
name: "overwriteChecker"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: () => argTargetDir || isValidPackageName(getProjectName()) ? null : "text",
|
|
146
|
+
name: "packageName",
|
|
147
|
+
message: "Package name:",
|
|
148
|
+
initial: () => toValidPackageName(getProjectName()),
|
|
149
|
+
validate: (dir) => isValidPackageName(dir) || "Invalid package.json name"
|
|
150
|
+
}
|
|
151
|
+
], { onCancel: () => {
|
|
152
|
+
throw new Error(`${COLOURS.red("✖")} Operation cancelled`);
|
|
153
|
+
} });
|
|
154
|
+
} catch (cancelled) {
|
|
155
|
+
if (cancelled instanceof Error) log.error(cancelled.message);
|
|
156
|
+
else log.error("Operation cancelled");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (!template) {
|
|
160
|
+
log.error("Operation cancelled");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const { overwrite, packageName } = template;
|
|
164
|
+
const root = path.join(cwd, targetDir);
|
|
165
|
+
const templateDir = path.resolve(__dirname, "..", TEMPLATE_NAME);
|
|
166
|
+
if (!fs.existsSync(templateDir)) throw new Error(`Template "${TEMPLATE_NAME}" not found`);
|
|
167
|
+
log.step("Creating project structure...");
|
|
168
|
+
if (!fs.existsSync(path.dirname(root))) try {
|
|
169
|
+
fs.mkdirSync(path.dirname(root), { recursive: true });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new Error(`Failed to create parent directory: ${error}`);
|
|
172
|
+
}
|
|
173
|
+
if (overwrite) try {
|
|
174
|
+
emptyDir(root);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error(`Failed to empty directory: ${error}`);
|
|
177
|
+
}
|
|
178
|
+
else if (!fs.existsSync(root)) try {
|
|
179
|
+
fs.mkdirSync(root, { recursive: true });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new Error(`Failed to create project directory: ${error}`);
|
|
182
|
+
}
|
|
183
|
+
log.info(`Scaffolding project in ${root}...`);
|
|
184
|
+
let pkgManager = "npm";
|
|
185
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) pkgManager = "pnpm";
|
|
186
|
+
else if (fs.existsSync(path.join(cwd, "yarn.lock"))) pkgManager = "yarn";
|
|
187
|
+
else if (process.env.npm_config_user_agent) {
|
|
188
|
+
const match = process.env.npm_config_user_agent.match(/^([^/]+)/);
|
|
189
|
+
if (match && [
|
|
190
|
+
"yarn",
|
|
191
|
+
"pnpm",
|
|
192
|
+
"npm"
|
|
193
|
+
].includes(match[1])) pkgManager = match[1];
|
|
194
|
+
}
|
|
195
|
+
const write = (file, content) => {
|
|
196
|
+
const targetPath = path.join(root, renameFiles[file] ?? file);
|
|
197
|
+
if (content) try {
|
|
198
|
+
fs.writeFileSync(targetPath, content);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(`Failed to write file "${targetPath}": ${error}`);
|
|
201
|
+
}
|
|
202
|
+
else copy(path.join(templateDir, file), targetPath);
|
|
203
|
+
};
|
|
204
|
+
let files;
|
|
205
|
+
try {
|
|
206
|
+
files = fs.readdirSync(templateDir);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw new Error(`Failed to read template directory: ${error}`);
|
|
209
|
+
}
|
|
210
|
+
for (const file of files.filter((f) => f !== "package.json")) write(file);
|
|
211
|
+
let pkgJson;
|
|
212
|
+
try {
|
|
213
|
+
pkgJson = fs.readFileSync(path.join(templateDir, "package.json"), "utf-8");
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw new Error(`Failed to read template package.json: ${error}`);
|
|
216
|
+
}
|
|
217
|
+
const pkg = JSON.parse(pkgJson);
|
|
218
|
+
pkg.name = packageName || getProjectName();
|
|
219
|
+
write("package.json", JSON.stringify(pkg, null, 2) + "\n");
|
|
220
|
+
try {
|
|
221
|
+
setupElectron(root);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
try {
|
|
224
|
+
fs.rmSync(root, {
|
|
225
|
+
recursive: true,
|
|
226
|
+
force: true
|
|
227
|
+
});
|
|
228
|
+
} catch {}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
log.success("Project created successfully!");
|
|
232
|
+
log.info("Done. Now run:");
|
|
233
|
+
const cdProjectName = path.relative(cwd, root);
|
|
234
|
+
if (root !== cwd) log.info(` cd ${cdProjectName.includes(" ") ? `"${cdProjectName}"` : cdProjectName}`);
|
|
235
|
+
switch (pkgManager) {
|
|
236
|
+
case "yarn":
|
|
237
|
+
log.info(" yarn");
|
|
238
|
+
log.info(" yarn dev");
|
|
239
|
+
break;
|
|
240
|
+
case "pnpm":
|
|
241
|
+
log.info(" pnpm install");
|
|
242
|
+
log.info(" pnpm dev");
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
log.info(` ${pkgManager} install`);
|
|
246
|
+
log.info(` ${pkgManager} run dev`);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
log.info("");
|
|
250
|
+
}
|
|
251
|
+
function setupElectron(root) {
|
|
252
|
+
const sourceDir = path.resolve(__dirname, "..", "electron");
|
|
253
|
+
const electronDir = path.join(root, "electron");
|
|
254
|
+
const electronPkgPath = path.resolve(__dirname, "..", "electron/package.json");
|
|
255
|
+
if (!fs.existsSync(electronPkgPath)) throw new Error("electron/package.json not found");
|
|
256
|
+
let electronPkgContent;
|
|
257
|
+
try {
|
|
258
|
+
electronPkgContent = fs.readFileSync(electronPkgPath, "utf-8");
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new Error(`Failed to read electron package.json: ${error}`);
|
|
261
|
+
}
|
|
262
|
+
const pkg = JSON.parse(electronPkgContent);
|
|
263
|
+
try {
|
|
264
|
+
fs.mkdirSync(electronDir, { recursive: true });
|
|
265
|
+
for (const name of [
|
|
266
|
+
"electron-env.d.ts",
|
|
267
|
+
"main.ts",
|
|
268
|
+
"preload.ts"
|
|
269
|
+
]) fs.copyFileSync(path.join(sourceDir, name), path.join(electronDir, name));
|
|
270
|
+
const electronBuilderSrc = path.join(sourceDir, "electron-builder.json5");
|
|
271
|
+
if (fs.existsSync(electronBuilderSrc)) fs.copyFileSync(electronBuilderSrc, path.join(root, "electron-builder.json5"));
|
|
272
|
+
else log.warn("electron-builder.json5 not found in electron directory");
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new Error(`Failed to setup Electron files: ${error}`);
|
|
275
|
+
}
|
|
276
|
+
editFile(path.join(root, "package.json"), (content) => {
|
|
277
|
+
const json = JSON.parse(content);
|
|
278
|
+
json.main = "dist-electron/main.js";
|
|
279
|
+
if (!json.scripts.build?.includes("electron-builder")) json.scripts.build = `${json.scripts.build || ""} && electron-builder`.trim();
|
|
280
|
+
json.devDependencies.electron = pkg.devDependencies.electron;
|
|
281
|
+
json.devDependencies["electron-builder"] = pkg.devDependencies["electron-builder"];
|
|
282
|
+
json.devDependencies["vite-plugin-electron"] = pkg.devDependencies["vite-plugin-electron"];
|
|
283
|
+
return JSON.stringify(json, null, 2) + "\n";
|
|
284
|
+
});
|
|
285
|
+
const electronPlugin = `electron({
|
|
286
|
+
main: {
|
|
287
|
+
entry: 'electron/main.ts',
|
|
288
|
+
},
|
|
289
|
+
preload: {
|
|
290
|
+
input: path.join('${root.replace(/\\/g, "\\\\")}', 'electron/preload.ts'),
|
|
291
|
+
},
|
|
292
|
+
})`;
|
|
293
|
+
editFile(path.join(root, "vite.config.ts"), (content) => {
|
|
294
|
+
if (content.includes("vite-plugin-electron/simple")) return content;
|
|
295
|
+
const hasPathImport = /import\s+path\s+from\s+['"]node:path['"]/.test(content);
|
|
296
|
+
const hasElectronImport = /import\s+electron\s+from\s+['"]vite-plugin-electron\/simple['"]/.test(content);
|
|
297
|
+
const pluginMatch = content.match(/plugins:\s*\[([\s\S]*)\]/);
|
|
298
|
+
if (pluginMatch && !pluginMatch[1].includes("vite-plugin-electron")) {
|
|
299
|
+
const pluginsContent = pluginMatch[1].trim();
|
|
300
|
+
const newPluginsContent = pluginsContent ? `${pluginsContent}, ${electronPlugin}` : electronPlugin;
|
|
301
|
+
content = content.replace(pluginMatch[0], `plugins: [${newPluginsContent}]`);
|
|
302
|
+
const lines = content.split("\n");
|
|
303
|
+
const importInsertIndex = lines.findIndex((line, i) => i > 0 && line.trim() === "" && lines[i - 1].trim().startsWith("import ") && !lines[i + 1]?.trim().startsWith("import "));
|
|
304
|
+
if (importInsertIndex !== -1) {
|
|
305
|
+
const newImports = [];
|
|
306
|
+
if (!hasPathImport) newImports.push(`import path from 'node:path'`);
|
|
307
|
+
if (!hasElectronImport) newImports.push(`import electron from 'vite-plugin-electron/simple'`);
|
|
308
|
+
if (newImports.length > 0) {
|
|
309
|
+
lines.splice(importInsertIndex + 1, 0, ...newImports);
|
|
310
|
+
content = lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
let lastImportIndex = -1;
|
|
314
|
+
for (let i = lines.length - 1; i >= 0; i--) if (lines[i].trim().startsWith("import ")) {
|
|
315
|
+
lastImportIndex = i;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
if (lastImportIndex !== -1) {
|
|
319
|
+
const newImports = [];
|
|
320
|
+
if (!hasPathImport) newImports.push(`import path from 'node:path'`);
|
|
321
|
+
if (!hasElectronImport) newImports.push(`import electron from 'vite-plugin-electron/simple'`);
|
|
322
|
+
if (newImports.length > 0) {
|
|
323
|
+
lines.splice(lastImportIndex + 1, 0, "", ...newImports);
|
|
324
|
+
content = lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return content;
|
|
330
|
+
});
|
|
331
|
+
editFile(path.join(root, "tsconfig.json"), (content) => {
|
|
332
|
+
try {
|
|
333
|
+
const json = JSON.parse(content);
|
|
334
|
+
if (json.include) {
|
|
335
|
+
const includeValue = json.include;
|
|
336
|
+
if (Array.isArray(includeValue)) {
|
|
337
|
+
if (!includeValue.includes("electron")) json.include = [...includeValue, "electron"];
|
|
338
|
+
} else if (typeof includeValue === "string") {
|
|
339
|
+
if (!includeValue.includes("electron")) json.include = [includeValue, "electron"];
|
|
340
|
+
}
|
|
341
|
+
} else json.include = ["src", "electron"];
|
|
342
|
+
return JSON.stringify(json, null, 2) + "\n";
|
|
343
|
+
} catch {
|
|
344
|
+
return content.split("\n").map((line) => line.trimStart().startsWith("\"include\"") ? line.replace("]", ", \"electron\"]") : line).join("\n");
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
editFile(path.join(root, ".gitignore"), (content) => {
|
|
348
|
+
if (/dist-electron/.test(content) && /release/.test(content)) return content.endsWith("\n") ? content : content + "\n";
|
|
349
|
+
let newContent = content;
|
|
350
|
+
if (!newContent.endsWith("\n")) newContent += "\n";
|
|
351
|
+
if (!/dist-electron/.test(newContent)) newContent += "dist-electron\n";
|
|
352
|
+
if (!/release/.test(newContent)) newContent += "release\n";
|
|
353
|
+
return newContent;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
init().catch((e) => {
|
|
357
|
+
if (e instanceof Error) log.error(e.message);
|
|
358
|
+
else log.error("An unexpected error occurred");
|
|
359
|
+
});
|
|
360
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @see - https://www.electron.build/configuration/configuration
|
|
2
|
+
{
|
|
3
|
+
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
|
|
4
|
+
"appId": "com.your-company.your-app",
|
|
5
|
+
"asar": true,
|
|
6
|
+
"productName": "YourApp",
|
|
7
|
+
"directories": {
|
|
8
|
+
"output": "release/${version}"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"dist-electron"
|
|
13
|
+
],
|
|
14
|
+
"mac": {
|
|
15
|
+
"target": [
|
|
16
|
+
"dmg"
|
|
17
|
+
],
|
|
18
|
+
"artifactName": "${productName}-Mac-${version}-Installer.${ext}"
|
|
19
|
+
},
|
|
20
|
+
"win": {
|
|
21
|
+
"target": [
|
|
22
|
+
{
|
|
23
|
+
"target": "nsis",
|
|
24
|
+
"arch": [
|
|
25
|
+
"x64"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"artifactName": "${productName}-Windows-${version}-Setup.${ext}"
|
|
30
|
+
},
|
|
31
|
+
"nsis": {
|
|
32
|
+
"oneClick": false,
|
|
33
|
+
"perMachine": false,
|
|
34
|
+
"allowToChangeInstallationDirectory": true,
|
|
35
|
+
"deleteAppDataOnUninstall": false
|
|
36
|
+
},
|
|
37
|
+
"linux": {
|
|
38
|
+
"target": [
|
|
39
|
+
"AppImage"
|
|
40
|
+
],
|
|
41
|
+
"artifactName": "${productName}-Linux-${version}.${ext}"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/electron/main.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { app, BrowserWindow } from 'electron'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
process.env.APP_ROOT = path.join(__dirname, '..')
|
|
7
|
+
const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
|
|
8
|
+
const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
|
9
|
+
|
|
10
|
+
const VITE_PUBLIC = VITE_DEV_SERVER_URL
|
|
11
|
+
? path.join(process.env.APP_ROOT, 'public')
|
|
12
|
+
: RENDERER_DIST
|
|
13
|
+
|
|
14
|
+
process.env.VITE_PUBLIC = VITE_PUBLIC
|
|
15
|
+
|
|
16
|
+
let win: BrowserWindow | null
|
|
17
|
+
|
|
18
|
+
function getIconPath(): string {
|
|
19
|
+
if (process.platform === 'win32') return path.join(VITE_PUBLIC, 'icon.ico') // Windows
|
|
20
|
+
if (process.platform === 'linux') return path.join(VITE_PUBLIC, 'icon.png') // Linux
|
|
21
|
+
return path.join(VITE_PUBLIC, 'icon.icns') // macOS
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createWindow() {
|
|
25
|
+
win = new BrowserWindow({
|
|
26
|
+
icon: getIconPath(),
|
|
27
|
+
webPreferences: {
|
|
28
|
+
preload: path.join(__dirname, 'preload.mjs'), // Script executado antes do renderer
|
|
29
|
+
contextIsolation: true, // Isola os contextos JS do main e do renderer (segurança)
|
|
30
|
+
nodeIntegration: false, // Impede acesso direto às APIs do Node no renderer
|
|
31
|
+
sandbox: true, // Restringe ainda mais as permissões do renderer
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Em dev, carrega direto do servidor Vite (HMR); em prod, serve o HTML compilado
|
|
36
|
+
if (VITE_DEV_SERVER_URL) {
|
|
37
|
+
win.loadURL(VITE_DEV_SERVER_URL).catch(console.error)
|
|
38
|
+
} else {
|
|
39
|
+
win.loadFile(path.join(RENDERER_DIST, 'index.html')).catch(console.error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// No macOS o app permanece ativo mesmo sem janelas abertas (comportamento padrão do sistema)
|
|
44
|
+
app.on('window-all-closed', () => {
|
|
45
|
+
if (process.platform !== 'darwin') {
|
|
46
|
+
app.quit()
|
|
47
|
+
win = null
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
// No macOS, recriar a janela ao clicar no ícone do dock quando não há janelas abertas
|
|
51
|
+
app.on('activate', () => {
|
|
52
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
53
|
+
createWindow()
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
app.whenReady().then(createWindow)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { contextBridge, ipcRenderer } from 'electron'
|
|
2
|
+
|
|
3
|
+
// --------- Expõe algumas APIs para o processo Renderer ---------
|
|
4
|
+
contextBridge.exposeInMainWorld('ipcRenderer', {
|
|
5
|
+
on(...args: Parameters<typeof ipcRenderer.on>) {
|
|
6
|
+
const [channel, listener] = args
|
|
7
|
+
return ipcRenderer.on(channel, (event, ...params) => listener(event, ...params))
|
|
8
|
+
}
|
|
9
|
+
// Você pode expor outras APIs que precisar aqui.
|
|
10
|
+
// ...
|
|
11
|
+
})
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-electron-app",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI tool to scaffold Electron + Vite + React applications",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "João Braga",
|
|
8
|
+
"homepage": "https://github.com/joaomjbraga/vite-electron-app#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/joaomjbraga/vite-electron-app.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/joaomjbraga/vite-electron-app/issues"
|
|
15
|
+
},
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"vite-electron-app": "index.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"electron",
|
|
23
|
+
"template-*/**",
|
|
24
|
+
"index.js",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"electron",
|
|
29
|
+
"vite",
|
|
30
|
+
"react",
|
|
31
|
+
"typescript",
|
|
32
|
+
"starter",
|
|
33
|
+
"boilerplate",
|
|
34
|
+
"desktop",
|
|
35
|
+
"scaffold",
|
|
36
|
+
"create"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"preinstall": "npx only-allow pnpm",
|
|
40
|
+
"watch": "vite build --watch",
|
|
41
|
+
"build": "vite build",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"test:unit": "vitest run __tests__/unit",
|
|
45
|
+
"test:integration": "vitest run __tests__/integration",
|
|
46
|
+
"pretest": "pnpm build",
|
|
47
|
+
"prepublishOnly": "pnpm build && pnpm test"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"prompts": "^2.4.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^25.5.2",
|
|
57
|
+
"@types/prompts": "^2.4.9",
|
|
58
|
+
"electron": "^41.1.1",
|
|
59
|
+
"typescript": "^6.0.2",
|
|
60
|
+
"vite": "^8.0.3",
|
|
61
|
+
"vitest": "^4.1.2"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.vscode/*
|
|
17
|
+
!.vscode/extensions.json
|
|
18
|
+
.idea
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.suo
|
|
21
|
+
*.ntvs*
|
|
22
|
+
*.njsproj
|
|
23
|
+
*.sln
|
|
24
|
+
*.sw?
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Electron + Vite + React</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-react-typescript-starter",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^19.2.4",
|
|
13
|
+
"react-dom": "^19.2.4"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/react": "^19.2.14",
|
|
17
|
+
"@types/react-dom": "^19.2.3",
|
|
18
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
19
|
+
"typescript": "^6.0.2",
|
|
20
|
+
"vite": "^8.0.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFBD4F"></stop><stop offset="100%" stop-color="#FF980E"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-12.437 6.827-9.067l12.333 7.7c2.09 1.3 4.715.745 5.883-1.234l13.055-22.14c2.129-3.615 7.748-3.681 9.877-.048l11.236 19.316c1.922 3.309 5.947 3.309 7.869 0l11.01-18.684c2.154-3.649 7.815-3.649 9.969 0l11.47 19.457c2.018 3.427 6.043 3.387 8.046-.086l18.572-32.155c2.091-3.623 7.92-3.623 10.011 0l18.702 32.352c2.003 3.481 6.028 3.5 8.018.034l12.58-21.797c1.972-3.412 5.897-3.412 7.87 0l12.723 22.008c2.154 3.73 7.885 3.715 10.008-.033z"/><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114L167.86.116c.688-2.386-1.74-4.487-4.647-4.015l-25.912 4.167c-2.396.386-3.61-2.724-1.91-4.804z"/></svg>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import reactLogo from './assets/react.svg'
|
|
3
|
+
import viteLogo from '/vite.svg'
|
|
4
|
+
|
|
5
|
+
function App() {
|
|
6
|
+
const [count, setCount] = useState(0)
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<div>
|
|
11
|
+
<a href="https://electron-vite.github.io" target="_blank" rel="noopener noreferrer">
|
|
12
|
+
<img src={viteLogo} className="logo" alt="Electron Vite logo" />
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://react.dev" target="_blank" rel="noopener noreferrer">
|
|
15
|
+
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
16
|
+
</a>
|
|
17
|
+
</div>
|
|
18
|
+
<h1>Electron + Vite + React</h1>
|
|
19
|
+
<div className="card">
|
|
20
|
+
<button onClick={() => setCount((count) => count + 1)}>
|
|
21
|
+
count is {count}
|
|
22
|
+
</button>
|
|
23
|
+
<p>
|
|
24
|
+
Edit <code>src/App.tsx</code> and save to test HMR
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
<p className="read-the-docs">
|
|
28
|
+
Click on the Vite and React logos to learn more
|
|
29
|
+
</p>
|
|
30
|
+
</>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default App
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
font-weight: 400;
|
|
5
|
+
color-scheme: light dark;
|
|
6
|
+
color: rgba(255, 255, 255, 0.87);
|
|
7
|
+
background-color: #242424;
|
|
8
|
+
font-synthesis: none;
|
|
9
|
+
text-rendering: optimizeLegibility;
|
|
10
|
+
-webkit-font-smoothing: antialiased;
|
|
11
|
+
-moz-osx-font-smoothing: grayscale;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#root {
|
|
15
|
+
max-width: 1280px;
|
|
16
|
+
margin: 0 auto;
|
|
17
|
+
padding: 2rem;
|
|
18
|
+
text-align: center;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.logo {
|
|
22
|
+
height: 6em;
|
|
23
|
+
padding: 1.5em;
|
|
24
|
+
will-change: filter;
|
|
25
|
+
transition: filter 300ms;
|
|
26
|
+
}
|
|
27
|
+
.logo:hover {
|
|
28
|
+
filter: drop-shadow(0 0 2em #646cffaa);
|
|
29
|
+
}
|
|
30
|
+
.logo.react:hover {
|
|
31
|
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@keyframes logo-spin {
|
|
35
|
+
from {
|
|
36
|
+
transform: rotate(0deg);
|
|
37
|
+
}
|
|
38
|
+
to {
|
|
39
|
+
transform: rotate(360deg);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
44
|
+
a:nth-of-type(2) .logo {
|
|
45
|
+
animation: logo-spin infinite 20s linear;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.card {
|
|
50
|
+
padding: 2em;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.read-the-docs {
|
|
54
|
+
color: #888;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
a {
|
|
58
|
+
font-weight: 500;
|
|
59
|
+
color: #646cff;
|
|
60
|
+
text-decoration: inherit;
|
|
61
|
+
}
|
|
62
|
+
a:hover {
|
|
63
|
+
color: #535bf2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
body {
|
|
67
|
+
margin: 0;
|
|
68
|
+
display: flex;
|
|
69
|
+
place-items: center;
|
|
70
|
+
min-width: 320px;
|
|
71
|
+
min-height: 100vh;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
h1 {
|
|
75
|
+
font-size: 3.2em;
|
|
76
|
+
line-height: 1.1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
button {
|
|
80
|
+
border-radius: 8px;
|
|
81
|
+
border: 1px solid transparent;
|
|
82
|
+
padding: 0.6em 1.2em;
|
|
83
|
+
font-size: 1em;
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
font-family: inherit;
|
|
86
|
+
background-color: #1a1a1a;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
transition: border-color 0.25s;
|
|
89
|
+
}
|
|
90
|
+
button:hover {
|
|
91
|
+
border-color: #646cff;
|
|
92
|
+
}
|
|
93
|
+
button:focus,
|
|
94
|
+
button:focus-visible {
|
|
95
|
+
outline: 4px auto -webkit-focus-ring-color;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (prefers-color-scheme: light) {
|
|
99
|
+
:root {
|
|
100
|
+
color: #213547;
|
|
101
|
+
background-color: #ffffff;
|
|
102
|
+
}
|
|
103
|
+
a:hover {
|
|
104
|
+
color: #747bff;
|
|
105
|
+
}
|
|
106
|
+
button {
|
|
107
|
+
background-color: #f9f9f9;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"],
|
|
24
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
25
|
+
}
|