@archznn/xavva 1.7.0 → 1.8.1
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/README.md +95 -25
- package/package.json +1 -1
- package/src/commands/AuditCommand.ts +6 -6
- package/src/commands/BuildCommand.ts +3 -3
- package/src/commands/Command.ts +2 -2
- package/src/commands/CommandRegistry.ts +36 -0
- package/src/commands/DeployCommand.ts +146 -116
- package/src/commands/DoctorCommand.ts +105 -5
- package/src/commands/HelpCommand.ts +2 -1
- package/src/commands/RunCommand.ts +112 -36
- package/src/commands/StartCommand.ts +3 -1
- package/src/index.ts +42 -133
- package/src/services/AuditService.ts +7 -2
- package/src/services/BrowserService.ts +41 -0
- package/src/services/BuildCacheService.ts +83 -0
- package/src/services/BuildService.ts +105 -82
- package/src/services/EndpointService.ts +17 -0
- package/src/services/ProjectService.ts +126 -0
- package/src/services/TomcatService.ts +65 -67
- package/src/services/WatcherService.ts +78 -0
- package/src/types/config.ts +30 -1
- package/src/utils/config.ts +21 -17
- package/src/utils/ui.ts +15 -13
package/README.md
CHANGED
|
@@ -1,41 +1,111 @@
|
|
|
1
|
-
# XAVVA 🚀 (Windows Only) `v1.
|
|
1
|
+
# XAVVA 🚀 (Windows Only) `v1.8.1`
|
|
2
2
|
|
|
3
3
|
Xavva é uma CLI de alto desempenho construída com **Bun** para automatizar o ciclo de desenvolvimento de aplicações Java (Maven/Gradle) rodando no Apache Tomcat. Ela foi desenhada especificamente para desenvolvedores que buscam a velocidade de ambientes modernos (como Node.js/Vite) dentro do ecossistema Java Enterprise.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🛠️ Por que Xavva?
|
|
8
|
+
|
|
9
|
+
Desenvolver para Java/Tomcat tradicionalmente envolve ciclos lentos de `clean install`, `war deploy` e restarts de servidor. O Xavva quebra esse paradigma ao introduzir um fluxo de **Hot-Reload incremental**, onde apenas o que mudou é enviado ao servidor.
|
|
10
|
+
|
|
11
|
+
### ⚡ Funcionalidades de Elite
|
|
6
12
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- **🛡️ JAR Audit**: Analisa todas as dependências (`.jar`) da sua aplicação em busca de vulnerabilidades (CVEs).
|
|
13
|
+
- **Ultra-Fast Hot Swap**: Compilação incremental e injeção direta de arquivos `.class` e recursos (JSP, HTML, CSS, JS) no Tomcat em execução sem restart.
|
|
14
|
+
- **Gradle & Maven Native**: Suporte robusto para ambos os ecossistemas, incluindo extração automática de classpath para execução de classes standalone (`run`/`debug`).
|
|
15
|
+
- **DCEVM Integration**: O Xavva pode baixar e configurar automaticamente uma JDK com DCEVM (JetBrains Runtime), permitindo mudanças estruturais em classes (novos métodos/campos) em tempo real.
|
|
16
|
+
- **API Documentation (Swagger-like)**: Mapeamento estático de endpoints, métodos HTTP e parâmetros diretamente no terminal via `xavva docs`.
|
|
17
|
+
- **Live Reload Automático**: Sincronização inteligente que atualiza o browser (Chrome/Edge) após mudanças em JSPs ou recursos estáticos.
|
|
18
|
+
- **Segurança & Robustez**: Auditoria de dependências (`.jar`) e execução protegida contra *Command Injection* no PowerShell.
|
|
19
|
+
- **Pathing JAR (Windows)**: Contorna limites de caracteres do Windows em classpaths gigantes através de geração dinâmica de Manifestos compatíveis com a especificação Java.
|
|
20
|
+
- **Auto-Healing**: Diagnóstico e reparo automático de problemas comuns de ambiente, como encoding UTF-8 com BOM.
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 🚀 Começo Rápido
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
### Pré-requisitos
|
|
27
|
+
- **Windows** (Otimizado para PowerShell Core e Windows PowerShell)
|
|
28
|
+
- **Bun** instalado (`powershell -c "irm bun.sh/install.ps1 | iex"`)
|
|
29
|
+
- **Tomcat** configurado via variável de ambiente `TOMCAT_HOME` ou `CATALINA_HOME`.
|
|
30
|
+
|
|
31
|
+
### Instalação
|
|
32
|
+
```powershell
|
|
33
|
+
# Instalação global via NPM
|
|
21
34
|
npm install -g @archznn/xavva
|
|
22
35
|
|
|
23
|
-
# Ou
|
|
36
|
+
# Ou use diretamente via npx
|
|
24
37
|
npx @archznn/xavva dev
|
|
25
38
|
```
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
---
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
## 📖 Referência de Comandos
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
O Xavva utiliza uma arquitetura modular de comandos e serviços, garantindo alta performance e extensibilidade.
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
### 1. Modo Desenvolvimento (`xavva dev`)
|
|
47
|
+
O comando principal para o dia a dia. Ativa o monitoramento de arquivos e o Hot-Reload.
|
|
48
|
+
- **O que faz**: Compila Java, sincroniza recursos, limpa logs, inicia o Tomcat e monitora mudanças.
|
|
49
|
+
- **Flags úteis**:
|
|
50
|
+
- `--no-build`: Pula o build inicial.
|
|
51
|
+
- `--watch`: Ativa o modo de observação de arquivos (padrão em `dev`).
|
|
52
|
+
- `--port 8081`: Define uma porta específica para o Tomcat.
|
|
53
|
+
- `--dp 9000`: Altera a porta do Debugger (JPDA) (padrão 5005).
|
|
54
|
+
|
|
55
|
+
### 2. Execução de Classes (`xavva run` / `xavva debug`)
|
|
56
|
+
Executa classes Java standalone (`public static void main`) com resolução automática de dependências Maven ou Gradle.
|
|
57
|
+
- **Inteligência de Classpath**: Gera automaticamente um `classpath.jar` temporário (Pathing JAR) para evitar o erro de "Command line too long" no Windows.
|
|
58
|
+
- **Busca por Grep**: Se você fornecer apenas parte do nome da classe, o Xavva a encontrará recursivamente no projeto.
|
|
59
|
+
|
|
60
|
+
### 3. Documentação de API (`xavva docs`)
|
|
61
|
+
Gera uma documentação instantânea dos seus controladores Jersey/Spring no terminal.
|
|
62
|
+
- Mostra a URL completa, método HTTP e parâmetros (Path, Query, Body).
|
|
63
|
+
|
|
64
|
+
### 4. Diagnóstico e Reparo (`xavva doctor`)
|
|
65
|
+
Verifica se o seu ambiente está saudável.
|
|
66
|
+
- **`xavva doctor --fix`**:
|
|
67
|
+
- Instala o **JetBrains Runtime (DCEVM)** se necessário.
|
|
68
|
+
- Remove automaticamente o **BOM (Byte Order Mark)** de arquivos Java que causam erros de compilação.
|
|
69
|
+
- Configura o `JAVA_HOME` do sistema.
|
|
70
|
+
|
|
71
|
+
### 5. Auditoria de Segurança (`xavva audit`)
|
|
72
|
+
Analisa a pasta `WEB-INF/lib` em busca de JARs vulneráveis via integração com **OSV.dev**. Essencial para manter a integridade do projeto antes de deploys em produção.
|
|
73
|
+
|
|
74
|
+
### 6. Logs em Tempo Real (`xavva logs`)
|
|
75
|
+
Exibe os logs do Tomcat filtrando ruídos excessivos e destacando StackTraces importantes. Use `--grep "NomeDaClasse"` para focar em logs específicos.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🏗️ Arquitetura do Sistema
|
|
80
|
+
|
|
81
|
+
O Xavva foi refatorado para uma arquitetura de **Injeção de Dependências** e **Serviços Centralizados**:
|
|
82
|
+
|
|
83
|
+
- **CommandRegistry**: Gerenciamento modular de comandos via Command Pattern.
|
|
84
|
+
- **ProjectService**: Inteligência centralizada para descoberta de diretórios de build, artefatos e classpaths Java.
|
|
85
|
+
- **AuditService**: Segurança aprimorada com execução isolada e protegida no PowerShell.
|
|
86
|
+
- **TomcatService**: Gerenciamento do ciclo de vida do servidor com suporte a hotswap dinâmico.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## ⚙️ Configuração (Zero Config)
|
|
91
|
+
|
|
92
|
+
O Xavva funciona sem arquivos de configuração externos, baseando-se no ambiente:
|
|
93
|
+
|
|
94
|
+
| Variável | Descrição |
|
|
95
|
+
|----------|-----------|
|
|
96
|
+
| `TOMCAT_HOME` | Caminho raiz do seu Apache Tomcat. |
|
|
97
|
+
| `JAVA_HOME` | JDK utilizada para compilação e execução. |
|
|
98
|
+
|
|
99
|
+
**Dica**: O Xavva cria automaticamente uma pasta `.xavva` no seu projeto para cache e artefatos temporários, e a adiciona ao seu `.gitignore`.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🧩 Sincronização de Recursos
|
|
104
|
+
|
|
105
|
+
Ao editar um arquivo, o Xavva decide a melhor estratégia:
|
|
106
|
+
- **`.java`**: Compila apenas a classe e injeta o bytecode via `fastSync`.
|
|
107
|
+
- **`.jsp` / `.html` / `.css`**: Sincroniza o arquivo diretamente na pasta de deploy do Tomcat.
|
|
108
|
+
- **`pom.xml` / `build.gradle`**: Identifica mudanças estruturais e sugere um rebuild completo com invalidação de cache inteligente.
|
|
39
109
|
|
|
40
110
|
---
|
|
41
|
-
*Desenvolvido para transformar
|
|
111
|
+
*Desenvolvido para transformar o legado em produtivo. 🚀*
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import type { Command } from "./Command";
|
|
4
|
-
import type { AppConfig } from "../types/config";
|
|
4
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
5
5
|
import { AuditService, type JarAuditResult } from "../services/AuditService";
|
|
6
6
|
import { Logger } from "../utils/ui";
|
|
7
7
|
|
|
8
8
|
export class AuditCommand implements Command {
|
|
9
|
-
|
|
9
|
+
constructor(private auditService: AuditService) {}
|
|
10
|
+
|
|
11
|
+
async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
|
|
10
12
|
Logger.section("Vulnerability & JAR Audit");
|
|
11
13
|
|
|
12
|
-
let appName = config.project.appName;
|
|
14
|
+
let appName = args?.name || config.project.appName;
|
|
13
15
|
|
|
14
16
|
if (!appName) {
|
|
15
17
|
appName = this.inferFromArtifacts();
|
|
@@ -38,10 +40,8 @@ export class AuditCommand implements Command {
|
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
const auditService = new AuditService(config.tomcat);
|
|
42
|
-
|
|
43
43
|
try {
|
|
44
|
-
const results = await auditService.runAudit(appName);
|
|
44
|
+
const results = await this.auditService.runAudit(appName);
|
|
45
45
|
const vulnerable = results.filter(r => r.vulnerabilities.length > 0);
|
|
46
46
|
|
|
47
47
|
if (vulnerable.length === 0) {
|
|
@@ -4,15 +4,15 @@ import { BuildService } from "../services/BuildService";
|
|
|
4
4
|
import { Logger } from "../utils/ui";
|
|
5
5
|
|
|
6
6
|
export class BuildCommand implements Command {
|
|
7
|
+
constructor(private buildService: BuildService) {}
|
|
8
|
+
|
|
7
9
|
async execute(config: AppConfig): Promise<void> {
|
|
8
|
-
const builder = new BuildService(config.project, config.tomcat);
|
|
9
|
-
|
|
10
10
|
Logger.section("Build Only");
|
|
11
11
|
Logger.info("Tool", config.project.buildTool.toUpperCase());
|
|
12
12
|
if (config.project.profile) Logger.info("Profile", config.project.profile);
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
|
-
await
|
|
15
|
+
await this.buildService.runBuild();
|
|
16
16
|
Logger.success("Build completed successfully!");
|
|
17
17
|
} catch (error: any) {
|
|
18
18
|
Logger.error(error.message);
|
package/src/commands/Command.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
2
|
+
import type { Command } from "./Command";
|
|
3
|
+
import { Logger } from "../utils/ui";
|
|
4
|
+
import { HelpCommand } from "./HelpCommand";
|
|
5
|
+
|
|
6
|
+
export class CommandRegistry {
|
|
7
|
+
private commands = new Map<string, Command>();
|
|
8
|
+
|
|
9
|
+
register(name: string, command: Command) {
|
|
10
|
+
this.commands.set(name, command);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
has(name: string): boolean {
|
|
14
|
+
return this.commands.has(name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(name: string): Command | undefined {
|
|
18
|
+
return this.commands.get(name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async execute(name: string, config: AppConfig, args: CLIArguments) {
|
|
22
|
+
const command = this.commands.get(name);
|
|
23
|
+
if (!command) {
|
|
24
|
+
Logger.error(`Comando desconhecido: ${name}`);
|
|
25
|
+
await new HelpCommand().execute(config);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await command.execute(config, args);
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
Logger.error(`Erro ao executar comando '${name}': ${error.message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,68 +1,24 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import type { Command } from "./Command";
|
|
4
|
-
import type { AppConfig } from "../types/config";
|
|
4
|
+
import type { AppConfig, CLIArguments } from "../types/config";
|
|
5
5
|
import { BuildService } from "../services/BuildService";
|
|
6
6
|
import { TomcatService } from "../services/TomcatService";
|
|
7
7
|
import { Logger } from "../utils/ui";
|
|
8
8
|
import { EndpointService } from "../services/EndpointService";
|
|
9
|
+
import { BrowserService } from "../services/BrowserService";
|
|
9
10
|
|
|
10
11
|
export class DeployCommand implements Command {
|
|
11
|
-
constructor(private tomcat
|
|
12
|
+
constructor(private tomcat: TomcatService, private builder: BuildService) {}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const psCommand = `
|
|
19
|
-
$shell = New-Object -ComObject WScript.Shell
|
|
20
|
-
$process = Get-Process | Where-Object { $_.MainWindowTitle -match "Chrome" -or $_.MainWindowTitle -match "Edge" } | Select-Object -First 1
|
|
21
|
-
if ($process) {
|
|
22
|
-
$shell.AppActivate($process.Id)
|
|
23
|
-
Sleep -m 100
|
|
24
|
-
$shell.SendKeys("{F5}")
|
|
25
|
-
}
|
|
26
|
-
`;
|
|
27
|
-
Bun.spawn(["powershell", "-command", psCommand]);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async execute(config: AppConfig, incremental = false, isWatching = false): Promise<void> {
|
|
31
|
-
const tomcat = this.tomcat || new TomcatService(config.tomcat);
|
|
32
|
-
const builder = this.builder || new BuildService(config.project, config.tomcat);
|
|
14
|
+
async execute(config: AppConfig, args?: CLIArguments): Promise<void> {
|
|
15
|
+
const incremental = args?.watch && args?.incremental;
|
|
16
|
+
const isWatching = !!args?.watch;
|
|
17
|
+
const tomcat = this.tomcat;
|
|
18
|
+
const builder = this.builder;
|
|
33
19
|
|
|
34
20
|
if (!incremental) {
|
|
35
|
-
|
|
36
|
-
Logger.config("Watch Mode", isWatching ? "ON" : "OFF");
|
|
37
|
-
Logger.config("Debug", config.project.debug ? "ON (Port 5005)" : "OFF");
|
|
38
|
-
|
|
39
|
-
let javaBin = "java";
|
|
40
|
-
if (process.env.JAVA_HOME) {
|
|
41
|
-
const homeBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
|
|
42
|
-
if (fs.existsSync(homeBin)) javaBin = homeBin;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const javaVer = Bun.spawnSync([javaBin, "-version"]);
|
|
46
|
-
const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
|
|
47
|
-
const hasDcevm = output.includes("dcevm") ||
|
|
48
|
-
output.includes("jetbrains") ||
|
|
49
|
-
output.includes("trava") ||
|
|
50
|
-
output.includes("jbr");
|
|
51
|
-
|
|
52
|
-
if (!hasDcevm && isWatching) {
|
|
53
|
-
Logger.config("Hot Reload", "Standard (No structural changes)");
|
|
54
|
-
} else if (hasDcevm) {
|
|
55
|
-
Logger.config("Hot Reload", "Advanced (DCEVM Active)");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const srcPath = path.join(process.cwd(), "src");
|
|
59
|
-
if (fs.existsSync(srcPath)) {
|
|
60
|
-
const contextPath = (config.project.appName || "").replace(".war", "");
|
|
61
|
-
const endpoints = EndpointService.scan(srcPath, contextPath);
|
|
62
|
-
if (endpoints.length > 0) {
|
|
63
|
-
Logger.config("Endpoints", endpoints.length);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
21
|
+
this.logConfiguration(config, isWatching);
|
|
66
22
|
} else {
|
|
67
23
|
Logger.watcher("Change detected", "change");
|
|
68
24
|
}
|
|
@@ -72,19 +28,28 @@ export class DeployCommand implements Command {
|
|
|
72
28
|
|
|
73
29
|
if (!incremental) {
|
|
74
30
|
await tomcat.killConflict();
|
|
75
|
-
|
|
31
|
+
await tomcat.clearWebapps();
|
|
76
32
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
33
|
+
if (!config.project.skipBuild) {
|
|
34
|
+
Logger.watcher("Building project", "start");
|
|
35
|
+
await builder.runBuild(incremental);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!config.project.skipBuild) {
|
|
39
|
+
Logger.build("Full project build and environment ready");
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
if (!config.project.skipBuild) {
|
|
43
|
+
Logger.watcher("Incremental compilation", "start");
|
|
44
|
+
await builder.runBuild(incremental);
|
|
45
|
+
}
|
|
81
46
|
}
|
|
82
47
|
|
|
83
48
|
if (incremental) {
|
|
84
|
-
const actualAppFolder = await builder.syncClasses(
|
|
49
|
+
const actualAppFolder = await builder.syncClasses();
|
|
85
50
|
const actualContextPath = contextPath || actualAppFolder || "";
|
|
86
51
|
const actualAppUrl = `http://localhost:${config.tomcat.port}/${actualContextPath}`;
|
|
87
|
-
await
|
|
52
|
+
await BrowserService.reload(actualAppUrl);
|
|
88
53
|
Logger.watcher("Redeploy completed", "success");
|
|
89
54
|
return;
|
|
90
55
|
}
|
|
@@ -96,62 +61,41 @@ export class DeployCommand implements Command {
|
|
|
96
61
|
const finalContextPath = contextPath || artifactInfo.finalName.replace(".war", "");
|
|
97
62
|
const appWebappPath = path.join(config.tomcat.path, "webapps", finalContextPath);
|
|
98
63
|
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
64
|
+
if (artifactInfo.isDirectory) {
|
|
65
|
+
await builder.syncClasses(artifactInfo.path);
|
|
66
|
+
Logger.build("Exploded directory synced");
|
|
67
|
+
} else {
|
|
68
|
+
if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
|
|
69
|
+
|
|
70
|
+
const artifactStat = fs.statSync(artifactInfo.path);
|
|
71
|
+
const webappStat = fs.existsSync(appWebappPath) ? fs.statSync(appWebappPath) : null;
|
|
72
|
+
|
|
73
|
+
if (!webappStat || artifactStat.mtimeMs > webappStat.mtimeMs) {
|
|
74
|
+
try {
|
|
75
|
+
Bun.spawnSync(["jar", "xf", artifactInfo.path], { cwd: appWebappPath });
|
|
76
|
+
Logger.build("Artifacts deployed");
|
|
77
|
+
} catch (e) {
|
|
78
|
+
const extractCmd = `Expand-Archive -Path $env:ARTIFACT_PATH -DestinationPath $env:DEST_PATH -Force`;
|
|
79
|
+
Bun.spawnSync(["powershell", "-command", extractCmd], {
|
|
80
|
+
env: {
|
|
81
|
+
...process.env,
|
|
82
|
+
ARTIFACT_PATH: artifactInfo.path,
|
|
83
|
+
DEST_PATH: appWebappPath
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
Logger.build("Artifacts deployed (legacy mode)");
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
Logger.build("Webapp already up to date, skipping extraction");
|
|
90
|
+
}
|
|
108
91
|
}
|
|
109
92
|
|
|
110
|
-
|
|
111
|
-
if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
|
|
112
|
-
|
|
113
|
-
const xavvaProps = path.join(process.cwd(), ".xavva", "hotswap-agent.properties");
|
|
114
|
-
if (fs.existsSync(xavvaProps)) {
|
|
115
|
-
fs.copyFileSync(xavvaProps, path.join(webInfClassesDir, "hotswap-agent.properties"));
|
|
116
|
-
}
|
|
93
|
+
this.injectHotswapProperties(appWebappPath);
|
|
117
94
|
|
|
118
95
|
const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
|
|
119
96
|
|
|
120
97
|
tomcat.onReady = async () => {
|
|
121
|
-
|
|
122
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
123
|
-
const response = await fetch(finalAppUrl);
|
|
124
|
-
if (response.status < 500) {
|
|
125
|
-
const memory = await tomcat.getMemoryUsage();
|
|
126
|
-
Logger.health(finalAppUrl, "success");
|
|
127
|
-
Logger.health(`Status ${response.status}`, "success");
|
|
128
|
-
Logger.health(`Memory ${memory}`, "success");
|
|
129
|
-
|
|
130
|
-
if (!config.project.quiet) {
|
|
131
|
-
const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), finalContextPath);
|
|
132
|
-
if (endpoints.length > 0) {
|
|
133
|
-
Logger.newline();
|
|
134
|
-
Logger.log(`${Logger.C.cyan}◈ ENDPOINT MAP:${Logger.C.reset}`);
|
|
135
|
-
endpoints.forEach(e => Logger.log(`${Logger.C.dim}➜ ${Logger.C.reset}http://localhost:${config.tomcat.port}${e.fullPath}`));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (incremental) {
|
|
140
|
-
await this.reloadBrowser(finalAppUrl);
|
|
141
|
-
} else {
|
|
142
|
-
if (process.platform === 'win32') {
|
|
143
|
-
Bun.spawn(["cmd", "/c", "start", finalAppUrl]);
|
|
144
|
-
} else {
|
|
145
|
-
const start = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
146
|
-
Bun.spawn([start, finalAppUrl]);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
Logger.health(`App returned status ${response.status}`, "warn");
|
|
151
|
-
}
|
|
152
|
-
} catch (e) {
|
|
153
|
-
Logger.health(`Could not connect to ${finalAppUrl}`, "error");
|
|
154
|
-
}
|
|
98
|
+
await this.handleServerReady(config, finalAppUrl, finalContextPath, tomcat, incremental);
|
|
155
99
|
};
|
|
156
100
|
|
|
157
101
|
tomcat.start(config, isWatching);
|
|
@@ -161,9 +105,98 @@ export class DeployCommand implements Command {
|
|
|
161
105
|
}
|
|
162
106
|
}
|
|
163
107
|
|
|
108
|
+
private logConfiguration(config: AppConfig, isWatching: boolean) {
|
|
109
|
+
Logger.config("Runtime", config.project.buildTool.toUpperCase());
|
|
110
|
+
Logger.config("Watch Mode", isWatching ? "ON" : "OFF");
|
|
111
|
+
Logger.config("Debug", config.project.debug ? `ON (Port ${config.project.debugPort})` : "OFF");
|
|
112
|
+
|
|
113
|
+
let javaBin = "java";
|
|
114
|
+
if (process.env.JAVA_HOME) {
|
|
115
|
+
const homeBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
|
|
116
|
+
if (fs.existsSync(homeBin)) javaBin = homeBin;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const javaVer = Bun.spawnSync([javaBin, "-version"]);
|
|
120
|
+
const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
|
|
121
|
+
const hasDcevm = ["dcevm", "jetbrains", "trava", "jbr"].some(v => output.includes(v));
|
|
122
|
+
|
|
123
|
+
if (!hasDcevm && isWatching) {
|
|
124
|
+
Logger.config("Hot Reload", "Standard (No structural changes)");
|
|
125
|
+
} else if (hasDcevm) {
|
|
126
|
+
Logger.config("Hot Reload", "Advanced (DCEVM Active)");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const srcPath = path.join(process.cwd(), "src");
|
|
130
|
+
if (fs.existsSync(srcPath)) {
|
|
131
|
+
const contextPath = (config.project.appName || "").replace(".war", "");
|
|
132
|
+
const endpoints = EndpointService.scan(srcPath, contextPath);
|
|
133
|
+
if (endpoints.length > 0) {
|
|
134
|
+
Logger.config("Endpoints", endpoints.length);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private injectHotswapProperties(appWebappPath: string) {
|
|
140
|
+
const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
|
|
141
|
+
if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const xavvaProps = path.join(process.cwd(), ".xavva", "hotswap-agent.properties");
|
|
144
|
+
if (fs.existsSync(xavvaProps)) {
|
|
145
|
+
fs.copyFileSync(xavvaProps, path.join(webInfClassesDir, "hotswap-agent.properties"));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async handleServerReady(config: AppConfig, url: string, context: string, tomcat: TomcatService, incremental: boolean) {
|
|
150
|
+
try {
|
|
151
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
152
|
+
const response = await fetch(url);
|
|
153
|
+
if (response.status < 500) {
|
|
154
|
+
const memory = await tomcat.getMemoryUsage();
|
|
155
|
+
Logger.health(url, "success");
|
|
156
|
+
Logger.health(`Status ${response.status}`, "success");
|
|
157
|
+
Logger.health(`Memory ${memory}`, "success");
|
|
158
|
+
|
|
159
|
+
if (!config.project.quiet) {
|
|
160
|
+
this.showEndpointMap(config.tomcat.port, context);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (incremental) {
|
|
164
|
+
await BrowserService.reload(url);
|
|
165
|
+
} else {
|
|
166
|
+
BrowserService.open(url);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
Logger.health(`App returned status ${response.status}`, "warn");
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
Logger.health(`Could not connect to ${url}`, "error");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private showEndpointMap(port: number, context: string) {
|
|
177
|
+
const endpoints = EndpointService.scan(path.join(process.cwd(), "src"), context);
|
|
178
|
+
if (endpoints.length > 0) {
|
|
179
|
+
Logger.newline();
|
|
180
|
+
Logger.log(`${Logger.C.cyan}◈ ENDPOINT MAP:${Logger.C.reset}`);
|
|
181
|
+
|
|
182
|
+
const apis = endpoints.filter(e => e.className !== "JSP");
|
|
183
|
+
const jsps = endpoints.filter(e => e.className === "JSP");
|
|
184
|
+
|
|
185
|
+
if (apis.length > 0) {
|
|
186
|
+
const uniqueApiUrls = [...new Set(apis.map(e => `http://localhost:${port}${e.fullPath}`))];
|
|
187
|
+
uniqueApiUrls.forEach(url => Logger.log(`${Logger.C.dim}➜ ${Logger.C.reset}${url}`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (jsps.length > 0) {
|
|
191
|
+
Logger.log(`${Logger.C.dim}--- JSPs ---${Logger.C.reset}`);
|
|
192
|
+
const uniqueJspUrls = [...new Set(jsps.map(e => `http://localhost:${port}${e.fullPath}`))];
|
|
193
|
+
uniqueJspUrls.forEach(url => Logger.log(`${Logger.C.dim}📄 ${Logger.C.reset}${url}`));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
164
198
|
async syncResource(config: AppConfig, filename: string): Promise<void> {
|
|
165
199
|
const contextPath = (config.project.appName || "").replace(".war", "");
|
|
166
|
-
|
|
167
200
|
const webappsPath = path.join(config.tomcat.path, "webapps");
|
|
168
201
|
let appFolder = contextPath;
|
|
169
202
|
|
|
@@ -174,10 +207,7 @@ export class DeployCommand implements Command {
|
|
|
174
207
|
}
|
|
175
208
|
|
|
176
209
|
const explodedPath = path.join(webappsPath, appFolder);
|
|
177
|
-
|
|
178
|
-
if (!appFolder || !fs.existsSync(explodedPath)) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
210
|
+
if (!appFolder || !fs.existsSync(explodedPath)) return;
|
|
181
211
|
|
|
182
212
|
const parts = filename.split(/[/\\]/);
|
|
183
213
|
const webappIndex = parts.indexOf("webapp");
|
|
@@ -196,7 +226,7 @@ export class DeployCommand implements Command {
|
|
|
196
226
|
if (!config.project.quiet) Logger.success(`Synced ${path.basename(filename)} directly to Tomcat!`);
|
|
197
227
|
|
|
198
228
|
const appUrl = `http://localhost:${config.tomcat.port}/${appFolder}`;
|
|
199
|
-
await
|
|
229
|
+
await BrowserService.reload(appUrl);
|
|
200
230
|
} catch (e) {
|
|
201
231
|
Logger.error(`Failed to sync resource: ${filename}`);
|
|
202
232
|
}
|