@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.
@@ -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
- if (this.projectConfig.clean) command.push("clean");
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
- if (this.projectConfig.clean) command.push("clean");
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 estáveis do Tomcat (atualizadas: 2026-03-04)
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 || "10.1.52";
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
- // Se a versão não está na lista, usa URL padrão
65
- const versionInfo = EmbeddedTomcatService.VERSIONS[this.version];
59
+ // Constrói URL de download baseada na plataforma
60
+ const versionInfo = isSupportedTomcatVersion(this.version) ? { sha512: "" } : null;
66
61
  if (versionInfo) {
67
- this.downloadUrl = versionInfo.url;
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
- const majorVersion = this.version.split(".")[0];
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
- const catalinaBat = path.join(this.tomcatHome, "bin", "catalina.bat");
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 catalinaBat = path.join(
104
- baseDir,
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 zipPath = path.join(
137
- this.baseDir,
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 netstat = spawn("cmd", [
304
- "/c",
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
- netstat.stdout?.on("data", (data) => {
294
+ checkProcess.stdout?.on("data", (data) => {
310
295
  output += data.toString();
311
296
  });
312
297
 
313
- netstat.on("close", () => {
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
- netstat.on("error", () => {
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 Object.keys(EmbeddedTomcatService.VERSIONS);
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 usando PowerShell
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 ps = spawn("powershell", [
392
- "-command",
393
- `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`,
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
- ps.on("close", (code) => {
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
- ps.on("error", (err) => {
401
+ extractProcess.on("error", (err) => {
407
402
  spinner(false);
408
403
  reject(err);
409
404
  });