@archznn/xavva 2.5.0 → 2.7.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/README.md +80 -4
- package/package.json +4 -2
- package/src/commands/DeployCommand.ts +29 -16
- package/src/commands/DoctorCommand.ts +129 -76
- package/src/commands/EncodingCommand.ts +351 -0
- package/src/commands/HelpCommand.ts +15 -0
- package/src/commands/LogsCommand.ts +1 -1
- package/src/commands/RunCommand.ts +21 -11
- package/src/config/versions.ts +63 -0
- package/src/di/container.ts +226 -0
- package/src/errors/ErrorHandler.ts +249 -0
- package/src/errors/XavvaError.ts +273 -0
- package/src/index.ts +98 -96
- package/src/services/AuditService.ts +3 -2
- package/src/services/BrowserService.ts +131 -17
- package/src/services/BuildService.ts +29 -2
- package/src/services/DeployWatcher.ts +183 -0
- package/src/services/EmbeddedTomcatService.ts +48 -53
- package/src/services/EncodingService.ts +548 -0
- package/src/services/FileWatcher.ts +243 -0
- package/src/services/LogAnalyzer.ts +4 -4
- package/src/services/TomcatService.ts +94 -17
- package/src/types/args.ts +136 -0
- package/src/types/config.ts +6 -0
- package/src/types/index.ts +7 -0
- package/src/utils/PathUtils.ts +221 -0
- package/src/utils/config.ts +6 -0
- package/src/utils/parsers/JavaParser.ts +413 -0
- package/src/utils/platform.ts +323 -0
- package/src/services/WatcherService.ts +0 -117
|
@@ -30,6 +30,11 @@ export class BuildService {
|
|
|
30
30
|
this.cache.clearCache();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Sempre limpa a pasta de build antes (target/ ou build/)
|
|
34
|
+
if (!incremental) {
|
|
35
|
+
await this.cleanBuildDirectory();
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
// Cache só é usado se --cache for passado ou em modo incremental
|
|
34
39
|
const useCache = this.projectConfig.cache || incremental;
|
|
35
40
|
|
|
@@ -54,7 +59,8 @@ export class BuildService {
|
|
|
54
59
|
if (incremental) {
|
|
55
60
|
command.push("compile");
|
|
56
61
|
} else {
|
|
57
|
-
|
|
62
|
+
// Sempre executa clean antes do build
|
|
63
|
+
command.push("clean");
|
|
58
64
|
// Use 'package' para gerar .war ou 'war:exploded' para pasta
|
|
59
65
|
if (this.projectConfig.war) {
|
|
60
66
|
command.push("package");
|
|
@@ -76,7 +82,8 @@ export class BuildService {
|
|
|
76
82
|
if (incremental) {
|
|
77
83
|
command.push("classes");
|
|
78
84
|
} else {
|
|
79
|
-
|
|
85
|
+
// Sempre executa clean antes do build
|
|
86
|
+
command.push("clean");
|
|
80
87
|
command.push("war");
|
|
81
88
|
command.push("--parallel", "--build-cache");
|
|
82
89
|
}
|
|
@@ -123,6 +130,26 @@ export class BuildService {
|
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Limpa fisicamente o diretório de build (target/ ou build/)
|
|
135
|
+
* Garante build limpo antes de cada execução
|
|
136
|
+
*/
|
|
137
|
+
private async cleanBuildDirectory(): Promise<void> {
|
|
138
|
+
const buildDir = this.projectConfig.buildTool === 'maven'
|
|
139
|
+
? path.join(process.cwd(), 'target')
|
|
140
|
+
: path.join(process.cwd(), 'build');
|
|
141
|
+
|
|
142
|
+
if (existsSync(buildDir)) {
|
|
143
|
+
try {
|
|
144
|
+
Logger.step(`Cleaning ${path.basename(buildDir)}/ directory...`);
|
|
145
|
+
await fs.rm(buildDir, { recursive: true, force: true });
|
|
146
|
+
Logger.debug(`Removed ${buildDir}`);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
Logger.warn(`Could not fully remove ${buildDir}, continuing...`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
async syncExploded(srcDir: string, destDir: string): Promise<void> {
|
|
127
154
|
if (!existsSync(srcDir)) return;
|
|
128
155
|
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeployWatcher - Específico para watch de deploy
|
|
3
|
+
* Usa FileWatcher genérico e adiciona lógica de deploy
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FileWatcher, type FileChangeEvent } from "./FileWatcher";
|
|
7
|
+
import { DeployCommand } from "../commands/DeployCommand";
|
|
8
|
+
import { Logger } from "../utils/ui";
|
|
9
|
+
import type { AppConfig } from "../types/config";
|
|
10
|
+
import { WATCHER_DEBOUNCE_MS, WATCHER_COOLING_MS } from "../utils/constants";
|
|
11
|
+
|
|
12
|
+
export class DeployWatcher {
|
|
13
|
+
private fileWatcher: FileWatcher;
|
|
14
|
+
private isDeploying = false;
|
|
15
|
+
private pendingFullBuild = false;
|
|
16
|
+
private modifiedFiles = new Set<string>();
|
|
17
|
+
private pendingFiles = new Set<string>();
|
|
18
|
+
private hasPendingChanges = false;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private config: AppConfig,
|
|
22
|
+
private deployCmd: DeployCommand
|
|
23
|
+
) {
|
|
24
|
+
this.fileWatcher = new FileWatcher({
|
|
25
|
+
recursive: true,
|
|
26
|
+
debounceMs: WATCHER_DEBOUNCE_MS,
|
|
27
|
+
coolingMs: WATCHER_COOLING_MS,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inicia o watch de deploy
|
|
33
|
+
*/
|
|
34
|
+
async start(): Promise<void> {
|
|
35
|
+
// Executa deploy inicial
|
|
36
|
+
await this.run(false);
|
|
37
|
+
|
|
38
|
+
// Configura handlers
|
|
39
|
+
this.setupHandlers();
|
|
40
|
+
|
|
41
|
+
// Inicia o watcher
|
|
42
|
+
this.fileWatcher.start();
|
|
43
|
+
|
|
44
|
+
Logger.info("DeployWatcher", "Monitorando alterações...");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Para o watching
|
|
49
|
+
*/
|
|
50
|
+
stop(): void {
|
|
51
|
+
this.fileWatcher.stop();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Configura handlers para diferentes tipos de arquivos
|
|
56
|
+
*/
|
|
57
|
+
private setupHandlers(): void {
|
|
58
|
+
// Handler para configurações de build
|
|
59
|
+
this.fileWatcher.on(/(pom\.xml|build\.gradle|build\.gradle\.kts)$/, (event) => {
|
|
60
|
+
this.handleBuildConfigChange(event);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Handler para arquivos Java
|
|
64
|
+
this.fileWatcher.on(/\.java$/, (event) => {
|
|
65
|
+
this.handleJavaChange(event);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Handler para recursos estáticos (JSP, HTML, CSS, etc)
|
|
69
|
+
this.fileWatcher.on(/\.(jsp|html|css|js|xml|properties)$/, (event) => {
|
|
70
|
+
this.handleResourceChange(event);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Trata mudança em arquivo de configuração de build
|
|
76
|
+
*/
|
|
77
|
+
private async handleBuildConfigChange(event: FileChangeEvent): Promise<void> {
|
|
78
|
+
if (!event.filename) return;
|
|
79
|
+
|
|
80
|
+
Logger.watcher(`Build configuration changed: ${event.filename}`, 'warn');
|
|
81
|
+
|
|
82
|
+
// Limpa cache quando config muda
|
|
83
|
+
const { BuildCacheService } = await import("./BuildCacheService");
|
|
84
|
+
new BuildCacheService().clearCache();
|
|
85
|
+
|
|
86
|
+
this.pendingFullBuild = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Trata mudança em arquivo Java
|
|
91
|
+
*/
|
|
92
|
+
private handleJavaChange(event: FileChangeEvent): void {
|
|
93
|
+
if (!event.filename || this.isDeploying) {
|
|
94
|
+
if (event.filename) {
|
|
95
|
+
this.pendingFiles.add(event.filename);
|
|
96
|
+
this.hasPendingChanges = true;
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Logger.watcher(event.filename, 'watch');
|
|
102
|
+
this.modifiedFiles.add(event.filename);
|
|
103
|
+
|
|
104
|
+
// Debounce para acumular múltiplas mudanças
|
|
105
|
+
this.scheduleDeploy();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Trata mudança em recurso estático
|
|
110
|
+
*/
|
|
111
|
+
private async handleResourceChange(event: FileChangeEvent): Promise<void> {
|
|
112
|
+
if (!event.filename) return;
|
|
113
|
+
|
|
114
|
+
Logger.watcher(event.filename, 'resource');
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await this.deployCmd.syncResource(this.config, event.filename);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
Logger.error(`Falha ao sincronizar recurso: ${event.filename}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Agenda deploy após debounce
|
|
125
|
+
*/
|
|
126
|
+
private scheduleDeploy(): void {
|
|
127
|
+
// O debounce já é feito pelo FileWatcher
|
|
128
|
+
// Aqui apenas executamos o deploy
|
|
129
|
+
const filesToCompile = [...this.modifiedFiles];
|
|
130
|
+
this.modifiedFiles.clear();
|
|
131
|
+
|
|
132
|
+
this.run(this.pendingFullBuild ? false : true, filesToCompile);
|
|
133
|
+
this.pendingFullBuild = false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Executa o deploy
|
|
138
|
+
*/
|
|
139
|
+
private async run(incremental = false, changedFiles?: string[]): Promise<void> {
|
|
140
|
+
if (this.isDeploying) return;
|
|
141
|
+
|
|
142
|
+
this.isDeploying = true;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await this.deployCmd.execute(this.config, {
|
|
146
|
+
watch: true,
|
|
147
|
+
incremental,
|
|
148
|
+
changedFiles,
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Erro já é tratado pelo comando
|
|
152
|
+
} finally {
|
|
153
|
+
this.isDeploying = false;
|
|
154
|
+
|
|
155
|
+
// Processa mudanças pendentes
|
|
156
|
+
if (this.hasPendingChanges) {
|
|
157
|
+
const pending = [...this.pendingFiles];
|
|
158
|
+
this.pendingFiles.clear();
|
|
159
|
+
this.hasPendingChanges = false;
|
|
160
|
+
|
|
161
|
+
Logger.watcher(`Processing ${pending.length} pending change(s)...`, 'warn');
|
|
162
|
+
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
this.run(true, pending);
|
|
165
|
+
}, 100);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Verifica se é arquivo de recurso (não Java)
|
|
172
|
+
*/
|
|
173
|
+
static isResourceFile(filename: string): boolean {
|
|
174
|
+
return /\.(jsp|html|css|js|xml|properties)$/.test(filename);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Verifica se é arquivo de configuração de build
|
|
179
|
+
*/
|
|
180
|
+
static isBuildConfig(filename: string): boolean {
|
|
181
|
+
return /^(pom\.xml|build\.gradle|build\.gradle\.kts)$/.test(filename);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Logger } from "../utils/ui";
|
|
2
|
+
import { VERSIONS, getAvailableTomcatVersions, isSupportedTomcatVersion } from "../config/versions";
|
|
2
3
|
import {
|
|
3
4
|
existsSync,
|
|
4
5
|
mkdirSync,
|
|
@@ -10,6 +11,17 @@ import {
|
|
|
10
11
|
import path from "path";
|
|
11
12
|
import os from "os";
|
|
12
13
|
import { spawn } from "child_process";
|
|
14
|
+
import {
|
|
15
|
+
getPlatform,
|
|
16
|
+
isWindows,
|
|
17
|
+
getTomcatArchiveName,
|
|
18
|
+
getTomcatDownloadUrl,
|
|
19
|
+
getTomcatArchiveUrl,
|
|
20
|
+
getExtractCommand,
|
|
21
|
+
getPortCheckCommand,
|
|
22
|
+
getCatalinaScript,
|
|
23
|
+
hasCatalinaScript,
|
|
24
|
+
} from "../utils/platform";
|
|
13
25
|
|
|
14
26
|
export interface EmbeddedTomcatOptions {
|
|
15
27
|
version?: string;
|
|
@@ -34,41 +46,24 @@ export class EmbeddedTomcatService {
|
|
|
34
46
|
private downloadUrl: string;
|
|
35
47
|
private isInstalled: boolean = false;
|
|
36
48
|
|
|
37
|
-
// Versões
|
|
38
|
-
private static readonly VERSIONS: Record<
|
|
39
|
-
string,
|
|
40
|
-
{ url: string; sha512: string }
|
|
41
|
-
> = {
|
|
42
|
-
"10.1.52": {
|
|
43
|
-
url: "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.52/bin/apache-tomcat-10.1.52-windows-x64.zip",
|
|
44
|
-
sha512: "",
|
|
45
|
-
},
|
|
46
|
-
"9.0.115": {
|
|
47
|
-
url: "https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.115/bin/apache-tomcat-9.0.115-windows-x64.zip",
|
|
48
|
-
sha512: "",
|
|
49
|
-
},
|
|
50
|
-
"11.0.18": {
|
|
51
|
-
url: "https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.18/bin/apache-tomcat-11.0.18-windows-x64.zip",
|
|
52
|
-
sha512: "",
|
|
53
|
-
},
|
|
54
|
-
};
|
|
49
|
+
// Versões agora centralizadas em src/config/versions.ts
|
|
55
50
|
|
|
56
51
|
constructor(options: EmbeddedTomcatOptions) {
|
|
57
|
-
this.version = options.version ||
|
|
52
|
+
this.version = options.version || VERSIONS.TOMCAT.DEFAULT;
|
|
58
53
|
this.port = options.port || 8080;
|
|
59
54
|
this.webappPath = path.resolve(options.webappPath);
|
|
60
55
|
this.contextPath = options.contextPath || "/";
|
|
61
56
|
this.baseDir = path.join(os.homedir(), ".xavva", "tomcat");
|
|
62
57
|
this.tomcatHome = path.join(this.baseDir, this.version);
|
|
63
58
|
|
|
64
|
-
//
|
|
65
|
-
const versionInfo =
|
|
59
|
+
// Constrói URL de download baseada na plataforma
|
|
60
|
+
const versionInfo = isSupportedTomcatVersion(this.version) ? { sha512: "" } : null;
|
|
66
61
|
if (versionInfo) {
|
|
67
|
-
|
|
62
|
+
// Usa URL primária (CDN Apache)
|
|
63
|
+
this.downloadUrl = getTomcatDownloadUrl(this.version);
|
|
68
64
|
} else {
|
|
69
|
-
// Tenta inferir URL baseado no padrão Apache
|
|
70
|
-
|
|
71
|
-
this.downloadUrl = `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${this.version}/bin/apache-tomcat-${this.version}-windows-x64.zip`;
|
|
65
|
+
// Tenta inferir URL baseado no padrão Apache (archive)
|
|
66
|
+
this.downloadUrl = getTomcatArchiveUrl(this.version);
|
|
72
67
|
}
|
|
73
68
|
}
|
|
74
69
|
|
|
@@ -76,8 +71,7 @@ export class EmbeddedTomcatService {
|
|
|
76
71
|
* Verifica se o Tomcat já está instalado
|
|
77
72
|
*/
|
|
78
73
|
checkInstallation(): boolean {
|
|
79
|
-
|
|
80
|
-
this.isInstalled = existsSync(catalinaBat);
|
|
74
|
+
this.isInstalled = hasCatalinaScript(this.tomcatHome);
|
|
81
75
|
return this.isInstalled;
|
|
82
76
|
}
|
|
83
77
|
|
|
@@ -100,13 +94,8 @@ export class EmbeddedTomcatService {
|
|
|
100
94
|
|
|
101
95
|
for (const entry of entries) {
|
|
102
96
|
if (entry.isDirectory()) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
entry.name,
|
|
106
|
-
"bin",
|
|
107
|
-
"catalina.bat",
|
|
108
|
-
);
|
|
109
|
-
if (existsSync(catalinaBat)) {
|
|
97
|
+
const tomcatPath = path.join(baseDir, entry.name);
|
|
98
|
+
if (hasCatalinaScript(tomcatPath)) {
|
|
110
99
|
versions.push(entry.name);
|
|
111
100
|
}
|
|
112
101
|
}
|
|
@@ -133,10 +122,8 @@ export class EmbeddedTomcatService {
|
|
|
133
122
|
mkdirSync(this.baseDir, { recursive: true });
|
|
134
123
|
}
|
|
135
124
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
`apache-tomcat-${this.version}.zip`,
|
|
139
|
-
);
|
|
125
|
+
const archiveName = getTomcatArchiveName(this.version);
|
|
126
|
+
const zipPath = path.join(this.baseDir, archiveName);
|
|
140
127
|
|
|
141
128
|
try {
|
|
142
129
|
// Download
|
|
@@ -300,21 +287,24 @@ export class EmbeddedTomcatService {
|
|
|
300
287
|
*/
|
|
301
288
|
async isPortAvailable(): Promise<boolean> {
|
|
302
289
|
return new Promise((resolve) => {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
`netstat -ano | findstr :${this.port}`,
|
|
306
|
-
]);
|
|
290
|
+
const cmd = getPortCheckCommand(this.port);
|
|
291
|
+
const checkProcess = spawn(cmd[0], cmd.slice(1));
|
|
307
292
|
let output = "";
|
|
308
293
|
|
|
309
|
-
|
|
294
|
+
checkProcess.stdout?.on("data", (data) => {
|
|
310
295
|
output += data.toString();
|
|
311
296
|
});
|
|
312
297
|
|
|
313
|
-
|
|
298
|
+
checkProcess.stderr?.on("data", (data) => {
|
|
299
|
+
output += data.toString();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
checkProcess.on("close", () => {
|
|
303
|
+
// Se houver output, a porta está em uso
|
|
314
304
|
resolve(output.trim().length === 0);
|
|
315
305
|
});
|
|
316
306
|
|
|
317
|
-
|
|
307
|
+
checkProcess.on("error", () => {
|
|
318
308
|
resolve(true); // Assume disponível se não conseguir verificar
|
|
319
309
|
});
|
|
320
310
|
});
|
|
@@ -350,7 +340,7 @@ export class EmbeddedTomcatService {
|
|
|
350
340
|
* Lista versões disponíveis
|
|
351
341
|
*/
|
|
352
342
|
static getAvailableVersions(): string[] {
|
|
353
|
-
return
|
|
343
|
+
return getAvailableTomcatVersions();
|
|
354
344
|
}
|
|
355
345
|
|
|
356
346
|
/**
|
|
@@ -382,18 +372,23 @@ export class EmbeddedTomcatService {
|
|
|
382
372
|
}
|
|
383
373
|
|
|
384
374
|
/**
|
|
385
|
-
* Extrai arquivo ZIP
|
|
375
|
+
* Extrai arquivo de arquivos (ZIP ou tar.gz)
|
|
386
376
|
*/
|
|
387
377
|
private async extractZip(zipPath: string, destDir: string): Promise<void> {
|
|
388
378
|
const spinner = Logger.spinner("Extraindo arquivos...");
|
|
389
379
|
|
|
390
380
|
return new Promise((resolve, reject) => {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
381
|
+
const cmd = getExtractCommand(zipPath, destDir);
|
|
382
|
+
|
|
383
|
+
if (!cmd) {
|
|
384
|
+
spinner(false);
|
|
385
|
+
reject(new Error(`Formato de arquivo não suportado: ${path.extname(zipPath)}`));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const extractProcess = spawn(cmd[0], cmd.slice(1));
|
|
395
390
|
|
|
396
|
-
|
|
391
|
+
extractProcess.on("close", (code) => {
|
|
397
392
|
if (code === 0) {
|
|
398
393
|
spinner(true);
|
|
399
394
|
resolve();
|
|
@@ -403,7 +398,7 @@ export class EmbeddedTomcatService {
|
|
|
403
398
|
}
|
|
404
399
|
});
|
|
405
400
|
|
|
406
|
-
|
|
401
|
+
extractProcess.on("error", (err) => {
|
|
407
402
|
spinner(false);
|
|
408
403
|
reject(err);
|
|
409
404
|
});
|