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 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
+ }
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite-plugin-electron/electron-env" />
2
+
3
+ declare namespace NodeJS {
4
+ interface ProcessEnv {
5
+ APP_ROOT: string
6
+ VITE_PUBLIC: string
7
+ }
8
+ }
9
+
10
+ interface Window {
11
+ ipcRenderer: import('electron').IpcRenderer
12
+ }
@@ -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,7 @@
1
+ {
2
+ "devDependencies": {
3
+ "electron": "^41.1.1",
4
+ "electron-builder": "^26.8.1",
5
+ "vite-plugin-electron": "^0.29.1"
6
+ }
7
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import './dist/index.mjs';
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
+ }
@@ -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,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.tsx'
4
+ import './globals.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true
9
+ },
10
+ "include": ["vite.config.ts"]
11
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })